# Description:

This notebook is calulating the ODE for the nine mode model created by Eckhardt et. al.:

**A low-dimensional model for turbulent shear flows**
https://iopscience.iop.org/article/10.1088/1367-2630/6/1/056

The evaluation of the accuring integrals that are optained by an galerkin projection are symbolically solved with the use of the **sympy** package.

```python
from IPython.display import display

import re
import glob
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import os
import itertools
import pickle
from tqdm.notebook import tqdm
from multiprocessing import Pool

import sympy
from sympy import N
from sympy import Function
from sympy import integrate
from sympy import simplify, trigsimp
from sympy import symbols, Symbol, Matrix, integrate
from sympy import sin, cos, sqrt, pi
from sympy.vector import gradient
from sympy.utilities.lambdify import lambdify

# you can remove that line if you sure
# another sympy version has no
# relevant changes in syntax
assert sympy.__version__ == '1.5.1'
```

## Set up folderstucture

```python
for p in ["expressions/I1","expressions/I2","expressions/I3"]:
    if not os.path.isdir(p):
        os.makedirs(p)
```

## Define variables and constants

```python
x, y, z = symbols('x y z')

a = Symbol(r'\alpha', nonzero=True,finite=True)
b = Symbol(r'\beta', nonzero=True,finite=True)
g = Symbol(r'\gamma', nonzero=True,finite=True)

Re = Symbol("Re")
```

# Navier Stokes solver

We are going to devide the calculation into three steps and solving the expressions I1, I2 and I3 seperatly (Compare to eq. 20 in the puplication):

\begin{equation}
\begin{split}
\left(
\frac{8\pi^2}{\alpha \gamma}
\right)
\dot{a}_m
=
-\sum_{m,l} a_m a_l  \int_\Omega \underbrace{\left[ (\boldsymbol{u}_m \cdot \nabla)\boldsymbol{u}_l\right] \cdot \boldsymbol{u}_n}_{\text{I1}} \text{ }d^3\boldsymbol{x}\\
+ \frac{1}{Re}\sum_{m} a_m  \int_\Omega \underbrace{(\nabla^2 \boldsymbol{u}_m) \cdot \boldsymbol{u}_n}_{\text{I2}} \text{ }d^3\boldsymbol{x}\\
+ \int_\Omega \underbrace{\boldsymbol{F} \cdot \boldsymbol{u}_n}_{\text{I3}} \text{ }d^3\boldsymbol{x}
\end{split}
\end{equation}

here the expanded calculation for the parts are defined:

```python
def calc_I1(um, ul, un):

    dx_ulx = ul[0].diff(x)
    dy_ulx = ul[0].diff(y)
    dz_ulx = ul[0].diff(z)

    dx_uly = ul[1].diff(x)
    dy_uly = ul[1].diff(y)
    dz_uly = ul[1].diff(z)

    dx_ulz = ul[2].diff(x)
    dy_ulz = ul[2].diff(y)
    dz_ulz = ul[2].diff(z)

    temp_vector = Matrix([
                         [um[0] * dx_ulx + um[1] * dy_ulx + um[2] * dz_ulx],
                         [um[0] * dx_uly + um[1] * dy_uly + um[2] * dz_uly],
                         [um[0] * dx_ulz + um[1] * dy_ulz + um[2] * dz_ulz]
                         ])

    return temp_vector.dot(un)

def calc_I2(um, un):

    d2x_ulx = um[0].diff(x,2)
    d2y_ulx = um[0].diff(y,2)
    d2z_ulx = um[0].diff(z,2)

    d2x_uly = um[1].diff(x,2)
    d2y_uly = um[1].diff(y,2)
    d2z_uly = um[1].diff(z,2)

    d2x_ulz = um[2].diff(x,2)
    d2y_ulz = um[2].diff(y,2)
    d2z_ulz = um[2].diff(z,2)

    temp_vector = Matrix([
                         [d2x_ulx + d2y_ulx + d2z_ulx],
                         [d2x_uly + d2y_uly + d2z_uly],
                         [d2x_ulz + d2y_ulz + d2z_ulz],
                         ])

    return temp_vector.dot(un)

def calc_I3(un, force):
    return force.dot(un)

def integrate_over_volumne(exp):
    return integrate(exp,(x, 0, 2*pi/a),(y, -1, 1),(z, 0, 2*pi/g))
```

# Define the nine modes and the driving force

```python
force = Matrix([
               [sqrt(2)*pi**2/(4*Re)*sin(pi*y/2)],
               [0],
               [0]
               ])
force
```

$\displaystyle \left[\begin{matrix}\frac{\sqrt{2} \pi^{2} \sin{\left(\frac{\pi y}{2} \right)}}{4 Re}\\0\\0\end{matrix}\right]$

```python
u1 = Matrix([
            [sqrt(2)*sin(pi * y/2)],
            [0],
            [0]
            ])

u2 = Matrix([
            [4/sqrt(3)*cos(pi * y/2)**2 * cos(g*z)],
            [0],
            [0]
            ])

u3 = Matrix([
            [0],
            [2*g*cos(pi*y/2)*cos(g*z)],
            [pi*sin(pi*y/2)*sin(g*z)]
            ])

u3 *= 2/sqrt(4*g**2 + pi**2)

u4 = Matrix([
            [0],
            [0],
            [4/sqrt(3)*cos(a*x)*cos(pi*y/2)**2]
            ])

u5 = Matrix([
            [0],
            [0],
            [2*sin(a*x)*sin(pi*y/2)]
            ])

u6 = Matrix([
            [-g*cos(a*x)* cos(pi*y/2)**2 * sin(g*z)],
            [0],
            [a*sin(a*x)*cos(pi*y/2)**2*cos(g*z)]
            ])

u6 *= 4*sqrt(2)/sqrt(3*(a**2+g**2))

u7 = Matrix([
            [g*sin(a*x)*sin(pi*y/2)*sin(g*z)],
            [0],
            [a*cos(a*x)*sin(pi*y/2)*cos(g*z)]
            ])

u7 *= 2*sqrt(2)/sqrt(a**2+g**2)

u8 = Matrix([
            [pi*a*sin(a*x)*sin(pi*y/2)*sin(g*z)],
            [2*(a**2+g**2)*cos(a*x)*cos(pi*y/2)*sin(g*z)],
            [-pi*g*cos(a*x)*sin(pi*y/2)*cos(g*z)]
            ])

u8 *= 2*sqrt(2)/sqrt((a**2+g**2)*(4*a**2+4*g**2+pi**2))

u9 = Matrix([
            [sqrt(2)*sin(3*pi*y/2)],
            [0],
            [0]
            ])

list_of_u = [u1,u2,u3,u4,u5,u6,u7,u8,u9]
```

## Check if the modes are divergenz free

```python
def divergence(f, x):
    return sum(fi.diff(xi) for fi, xi in zip(f, x))

for u in list_of_u:
    div = divergence(u, [x,y,z])
    div = simplify(div)
    div = trigsimp(div)

    assert div == 0
```

# Display the modes

```python
for u in list_of_u:
    display(u)
```

$\displaystyle \left[\begin{matrix}\sqrt{2} \sin{\left(\frac{\pi y}{2} \right)}\\0\\0\end{matrix}\right]$

$\displaystyle \left[\begin{matrix}\frac{4 \sqrt{3} \cos^{2}{\left(\frac{\pi y}{2} \right)} \cos{\left(\gamma z \right)}}{3}\\0\\0\end{matrix}\right]$

$\displaystyle \left[\begin{matrix}0\\\frac{4 \gamma \cos{\left(\frac{\pi y}{2} \right)} \cos{\left(\gamma z \right)}}{\sqrt{4 \gamma^{2} + \pi^{2}}}\\\frac{2 \pi \sin{\left(\frac{\pi y}{2} \right)} \sin{\left(\gamma z \right)}}{\sqrt{4 \gamma^{2} + \pi^{2}}}\end{matrix}\right]$

$\displaystyle \left[\begin{matrix}0\\0\\\frac{4 \sqrt{3} \cos^{2}{\left(\frac{\pi y}{2} \right)} \cos{\left(\alpha x \right)}}{3}\end{matrix}\right]$

$\displaystyle \left[\begin{matrix}0\\0\\2 \sin{\left(\frac{\pi y}{2} \right)} \sin{\left(\alpha x \right)}\end{matrix}\right]$

$\displaystyle \left[\begin{matrix}- \frac{4 \sqrt{2} \gamma \sin{\left(\gamma z \right)} \cos^{2}{\left(\frac{\pi y}{2} \right)} \cos{\left(\alpha x \right)}}{\sqrt{3 \alpha^{2} + 3 \gamma^{2}}}\\0\\\frac{4 \sqrt{2} \alpha \sin{\left(\alpha x \right)} \cos^{2}{\left(\frac{\pi y}{2} \right)} \cos{\left(\gamma z \right)}}{\sqrt{3 \alpha^{2} + 3 \gamma^{2}}}\end{matrix}\right]$

$\displaystyle \left[\begin{matrix}\frac{2 \sqrt{2} \gamma \sin{\left(\frac{\pi y}{2} \right)} \sin{\left(\alpha x \right)} \sin{\left(\gamma z \right)}}{\sqrt{\alpha^{2} + \gamma^{2}}}\\0\\\frac{2 \sqrt{2} \alpha \sin{\left(\frac{\pi y}{2} \right)} \cos{\left(\alpha x \right)} \cos{\left(\gamma z \right)}}{\sqrt{\alpha^{2} + \gamma^{2}}}\end{matrix}\right]$

$\displaystyle \left[\begin{matrix}\frac{2 \sqrt{2} \pi \alpha \sin{\left(\frac{\pi y}{2} \right)} \sin{\left(\alpha x \right)} \sin{\left(\gamma z \right)}}{\sqrt{\alpha^{2} + \gamma^{2}} \sqrt{4 \alpha^{2} + 4 \gamma^{2} + \pi^{2}}}\\\frac{2 \sqrt{2} \left(2 \alpha^{2} + 2 \gamma^{2}\right) \sin{\left(\gamma z \right)} \cos{\left(\frac{\pi y}{2} \right)} \cos{\left(\alpha x \right)}}{\sqrt{\alpha^{2} + \gamma^{2}} \sqrt{4 \alpha^{2} + 4 \gamma^{2} + \pi^{2}}}\\- \frac{2 \sqrt{2} \pi \gamma \sin{\left(\frac{\pi y}{2} \right)} \cos{\left(\alpha x \right)} \cos{\left(\gamma z \right)}}{\sqrt{\alpha^{2} + \gamma^{2}} \sqrt{4 \alpha^{2} + 4 \gamma^{2} + \pi^{2}}}\end{matrix}\right]$

$\displaystyle \left[\begin{matrix}\sqrt{2} \sin{\left(\frac{3 \pi y}{2} \right)}\\0\\0\end{matrix}\right]$

# Calculate I1 over all posible permutations
It will check if the result is allready in the folder ./expressions. If you want to re-run the calculation make sure those folders are emtpy.

```python
def wrapper(indices):

    m,l,n = indices

    out_path = "expressions/I1/I1_m={}_l={}_n={}.pkl".format(m,l,n)

    if not os.path.isfile(out_path):

        nonlin = calc_I1(list_of_u[m],list_of_u[l],list_of_u[n])
        nonlin = simplify(nonlin)
        nonlin = trigsimp(nonlin)

        integral = integrate_over_volumne(nonlin)
        integral = simplify(integral)
        integral = trigsimp(integral)

        with open(out_path, "w") as outf:
            pickle.dump(integral, outf)

# List all the differnt permutations for the indices
todolist = [p for p in itertools.product(range(9), repeat=3)]

print("Jobs to do: {}".format(len(todolist)))

# # Run jobs on one thread
# for item in tqdm(todolist):
#     wrapper(item)

# do the calculations in parallel
pool = Pool(processes=8)
for _ in tqdm(pool.imap_unordered(wrapper, todolist), total=len(todolist)):
    pass
```

    Jobs to do: 729

    HBox(children=(FloatProgress(value=0.0, max=729.0), HTML(value=u'')))

### Check Couplings

Those are the nonlinear couplings of the modes. Compare to **Table 1** in the puplication.

```python
all_couplings = glob.glob("expressions/I1/*.pkl")

table = np.zeros((9,9,9))

for coupling in all_couplings:

    # get the indices from the filename

    match = re.search("m=(\d+)",coupling)
    m = int(match.group(1))

    match = re.search("l=(\d+)",coupling)
    l = int(match.group(1))

    match = re.search("n=(\d+)",coupling)
    n = int(match.group(1))

    with open(coupling) as inf:
        out = pickle.load(inf)

    if out != 0:
        table[m,l,n] += 1

# some pandas magic to get the table from the 3d numpy array
df = pd.DataFrame(table.tolist())
df = df.applymap(np.nonzero)
df = df.applymap(lambda x: ', '.join(str(xx) for xx in x[0]))
df
```

      0
      1
      2
      3
      4
      5
      6
      7
      8

      0

      4
      3
      6
      5

      1

      5
      6, 7
      3
      4
      4

      2
      1
      0, 8

      6, 7
      5
      4
      3
      3
      1

      3

      5
      6, 7

      1
      2
      2

      4

      6, 7
      5

      2
      1
      1

      5

      4

      2

      6

      3
      2

      7
      5

      3
      2

      0, 8

      5

      8

      4
      3
      6
      5

# Calculate I2 over all posible permutations
It will check if the result is allready in the folder ./expressions. If you want to re-run the calculation make sure those folders are emtpy.

```python
def wrapper(indices):

    m,n = indices

    out_path = "expressions/I2/I2_m={}_n={}.pkl".format(m,n)

    if not os.path.isfile(out_path):

        diffusion = calc_I2(list_of_u[m],list_of_u[n])
        diffusion = simplify(diffusion)
        diffusion = trigsimp(diffusion)

        integral = integrate_over_volumne(diffusion)
        integral = simplify(integral)
        integral = trigsimp(integral)

        with open(out_path, "w") as outf:
            pickle.dump(integral, outf)

# List all the differnt permutations for the indices
todolist = [p for p in itertools.product(range(9), repeat=2)]

print("Jobs to do: {}".format(len(todolist)))

# # Run jobs on one thread
# for item in tqdm(todolist):
#     wrapper(item)

# do the calculations in parallel
pool = Pool(processes=8)
for _ in tqdm(pool.imap_unordered(wrapper, todolist), total=len(todolist)):
    pass
```

    Jobs to do: 81

    HBox(children=(FloatProgress(value=0.0, max=81.0), HTML(value=u'')))

# Calculate I3 over all posible permutations
It will check if the result is allready in the folder ./expressions. If you want to re-run the calculation make sure those folders are emtpy.

```python
def wrapper(indices):

    n = indices

    out_path = "expressions/I3/I3_n={}.pkl".format(n)

    if not os.path.isfile(out_path):

        diffusion = calc_I3(list_of_u[n],force)
        diffusion = simplify(diffusion)
        diffusion = trigsimp(diffusion)

        integral = integrate_over_volumne(diffusion)
        integral = simplify(integral)
        integral = trigsimp(integral)

        with open(out_path, "w") as outf:
            pickle.dump(integral, outf)

# List all the differnt permutations for the indices
todolist = range(9)

print("Jobs to do: {}".format(len(todolist)))

# # Run jobs on one thread
# for item in tqdm(todolist):
#     wrapper(item)

# do the calculations in parallel
pool = Pool(processes=8)
for _ in tqdm(pool.imap_unordered(wrapper, todolist), total=len(todolist)):
    pass
```

    Jobs to do: 9

    HBox(children=(FloatProgress(value=0.0, max=9.0), HTML(value=u'')))

# Build the equations

```python
a0 = Symbol("a0")
a1 = Symbol("a1")
a2 = Symbol("a2")
a3 = Symbol("a3")
a4 = Symbol("a4")
a5 = Symbol("a5")
a6 = Symbol("a6")
a7 = Symbol("a7")
a8 = Symbol("a8")

list_of_a = [a0,a1,a2,a3,a4,a5,a6,a7,a8]
```

```python
# define 1/constant of the left side of the NSE
C = a*g/(8*pi**2)
C
```

$\displaystyle \frac{\alpha \gamma}{8 \pi^{2}}$

```python
odes = [] # list for the nine odes

for n in range(9):

    ode = 0 # start with an emtpy expression

    # build the nonlinear part
    for m,l in [p for p in itertools.product(range(9), repeat=2)]:

        path = "expressions/I1/I1_m={}_l={}_n={}.pkl".format(m,l,n)
        with open(path) as inf:
            X = pickle.load(inf)

        ode += -C *list_of_a[m] * list_of_a[l] * X # add according to the NSE

    # build the linear part
    for m in range(9):

        path = "expressions/I2/I2_m={}_n={}.pkl".format(m,n)
        with open(path) as inf:
            X = pickle.load(inf)

        ode += 1/Re * C * list_of_a[m] * X # add according to the NSE

    # add the force coupling
    path = "expressions/I3/I3_n={}.pkl".format(n)
    with open(path) as inf:
        X = pickle.load(inf)

    ode += C * X # add according to the NSE

    odes.append(ode)
```

```python
# show the results
for ode in odes:
    display(ode)
    print(3*"\n")
```

$\displaystyle \frac{\sqrt{6} \pi \gamma a_{1} a_{2}}{2 \sqrt{4 \gamma^{2} + \pi^{2}}} - \frac{\sqrt{6} \pi \gamma a_{5} a_{7} \left(\alpha^{2} + \gamma^{2}\right)}{2 \sqrt{4 \alpha^{6} + 12 \alpha^{4} \gamma^{2} + \pi^{2} \alpha^{4} + 12 \alpha^{2} \gamma^{4} + 2 \pi^{2} \alpha^{2} \gamma^{2} + 4 \gamma^{6} + \pi^{2} \gamma^{4}}} - \frac{\pi^{2} a_{0}}{4 Re} + \frac{\pi^{2}}{4 Re}$

$\displaystyle - \frac{\sqrt{6} \pi \alpha \gamma a_{4} a_{7}}{6 \sqrt{4 \alpha^{4} + 8 \alpha^{2} \gamma^{2} + \pi^{2} \alpha^{2} + 4 \gamma^{4} + \pi^{2} \gamma^{2}}} + \frac{5 \sqrt{6} \gamma^{2} a_{3} a_{5}}{9 \sqrt{\alpha^{2} + \gamma^{2}}} - \frac{\sqrt{6} \gamma^{2} a_{4} a_{6}}{6 \sqrt{\alpha^{2} + \gamma^{2}}} - \frac{\sqrt{6} \pi \gamma a_{0} a_{2}}{2 \sqrt{4 \gamma^{2} + \pi^{2}}} - \frac{\sqrt{6} \pi \gamma a_{2} a_{8}}{2 \sqrt{4 \gamma^{2} + \pi^{2}}} - \frac{a_{1} \left(3 \gamma^{2} + \pi^{2}\right)}{3 Re}$

$\displaystyle \frac{\sqrt{6} \pi \alpha \gamma a_{3} a_{6}}{3 \sqrt{\alpha^{2} + \gamma^{2}} \sqrt{4 \gamma^{2} + \pi^{2}}} + \frac{\sqrt{6} \pi \alpha \gamma a_{4} a_{5}}{3 \sqrt{\alpha^{2} + \gamma^{2}} \sqrt{4 \gamma^{2} + \pi^{2}}} - \frac{\sqrt{6} \gamma^{2} a_{3} a_{7} \left(12 \alpha^{2} + 12 \gamma^{2} + \pi^{2}\right)}{6 \sqrt{16 \alpha^{4} \gamma^{2} + 4 \pi^{2} \alpha^{4} + 32 \alpha^{2} \gamma^{4} + 12 \pi^{2} \alpha^{2} \gamma^{2} + \pi^{4} \alpha^{2} + 16 \gamma^{6} + 8 \pi^{2} \gamma^{4} + \pi^{4} \gamma^{2}}} + \frac{\sqrt{6} \pi^{2} a_{3} a_{7} \left(4 \alpha^{2} + \frac{8 \gamma^{2}}{3}\right)}{8 \sqrt{16 \alpha^{4} \gamma^{2} + 4 \pi^{2} \alpha^{4} + 32 \alpha^{2} \gamma^{4} + 12 \pi^{2} \alpha^{2} \gamma^{2} + \pi^{4} \alpha^{2} + 16 \gamma^{6} + 8 \pi^{2} \gamma^{4} + \pi^{4} \gamma^{2}}} - \frac{a_{2} \left(4 \gamma^{2} + \pi^{2}\right)}{4 Re}$

$\displaystyle - \frac{5 \sqrt{6} \alpha^{2} a_{1} a_{5}}{9 \sqrt{\alpha^{2} + \gamma^{2}}} - \frac{\sqrt{6} \pi \alpha \gamma a_{2} a_{6}}{2 \sqrt{\alpha^{2} + \gamma^{2}} \sqrt{4 \gamma^{2} + \pi^{2}}} - \frac{\sqrt{6} \alpha a_{0} a_{4}}{6} - \frac{\sqrt{6} \alpha a_{4} a_{8}}{6} + \frac{\sqrt{6} \pi^{2} \gamma^{2} a_{2} a_{7}}{3 \sqrt{16 \alpha^{4} \gamma^{2} + 4 \pi^{2} \alpha^{4} + 32 \alpha^{2} \gamma^{4} + 12 \pi^{2} \alpha^{2} \gamma^{2} + \pi^{4} \alpha^{2} + 16 \gamma^{6} + 8 \pi^{2} \gamma^{4} + \pi^{4} \gamma^{2}}} - \frac{\sqrt{6} \pi^{2} a_{2} a_{7} \left(4 \alpha^{2} + \frac{8 \gamma^{2}}{3}\right)}{8 \sqrt{16 \alpha^{4} \gamma^{2} + 4 \pi^{2} \alpha^{4} + 32 \alpha^{2} \gamma^{4} + 12 \pi^{2} \alpha^{2} \gamma^{2} + \pi^{4} \alpha^{2} + 16 \gamma^{6} + 8 \pi^{2} \gamma^{4} + \pi^{4} \gamma^{2}}} - \frac{a_{3} \left(3 \alpha^{2} + \pi^{2}\right)}{3 Re}$

$\displaystyle \frac{\sqrt{6} \alpha^{2} a_{1} a_{6}}{6 \sqrt{\alpha^{2} + \gamma^{2}}} - \frac{\sqrt{6} \pi \alpha \gamma a_{1} a_{7}}{6 \sqrt{4 \alpha^{4} + 8 \alpha^{2} \gamma^{2} + \pi^{2} \alpha^{2} + 4 \gamma^{4} + \pi^{2} \gamma^{2}}} + \frac{\sqrt{6} \pi \alpha \gamma a_{2} a_{5}}{3 \sqrt{\alpha^{2} + \gamma^{2}} \sqrt{4 \gamma^{2} + \pi^{2}}} + \frac{\sqrt{6} \alpha a_{0} a_{3}}{6} + \frac{\sqrt{6} \alpha a_{3} a_{8}}{6} - \frac{a_{4} \left(4 \alpha^{2} + \pi^{2}\right)}{4 Re}$

$\displaystyle \frac{5 \sqrt{6} \alpha^{2} a_{1} a_{3}}{9 \sqrt{\alpha^{2} + \gamma^{2}}} - \frac{2 \sqrt{6} \pi \alpha \gamma a_{2} a_{4}}{3 \sqrt{\alpha^{2} + \gamma^{2}} \sqrt{4 \gamma^{2} + \pi^{2}}} + \frac{\sqrt{6} \alpha a_{0} a_{6} \left(\alpha^{2} + \gamma^{2}\right)}{6 \sqrt{\alpha^{4} + 2 \alpha^{2} \gamma^{2} + \gamma^{4}}} + \frac{\sqrt{6} \alpha a_{6} a_{8} \left(\alpha^{2} + \gamma^{2}\right)}{6 \sqrt{\alpha^{4} + 2 \alpha^{2} \gamma^{2} + \gamma^{4}}} - \frac{5 \sqrt{6} \gamma^{2} a_{1} a_{3}}{9 \sqrt{\alpha^{2} + \gamma^{2}}} + \frac{\sqrt{6} \pi \gamma a_{0} a_{7} \left(\alpha^{2} + \gamma^{2}\right)}{2 \sqrt{4 \alpha^{6} + 12 \alpha^{4} \gamma^{2} + \pi^{2} \alpha^{4} + 12 \alpha^{2} \gamma^{4} + 2 \pi^{2} \alpha^{2} \gamma^{2} + 4 \gamma^{6} + \pi^{2} \gamma^{4}}} + \frac{\sqrt{6} \pi \gamma a_{7} a_{8} \left(\alpha^{2} + \gamma^{2}\right)}{2 \sqrt{4 \alpha^{6} + 12 \alpha^{4} \gamma^{2} + \pi^{2} \alpha^{4} + 12 \alpha^{2} \gamma^{4} + 2 \pi^{2} \alpha^{2} \gamma^{2} + 4 \gamma^{6} + \pi^{2} \gamma^{4}}} - \frac{a_{5} \left(\alpha^{2} + \gamma^{2}\right) \left(3 \alpha^{2} + 3 \gamma^{2} + \pi^{2}\right)}{3 Re \sqrt{\alpha^{4} + 2 \alpha^{2} \gamma^{2} + \gamma^{4}}}$

$\displaystyle - \frac{\sqrt{6} \alpha^{2} a_{1} a_{4}}{6 \sqrt{\alpha^{2} + \gamma^{2}}} + \frac{\sqrt{6} \pi \alpha \gamma a_{2} a_{3}}{6 \sqrt{\alpha^{2} + \gamma^{2}} \sqrt{4 \gamma^{2} + \pi^{2}}} - \frac{\sqrt{6} \alpha a_{0} a_{5} \left(\alpha^{2} + \gamma^{2}\right)}{6 \sqrt{\alpha^{4} + 2 \alpha^{2} \gamma^{2} + \gamma^{4}}} - \frac{\sqrt{6} \alpha a_{5} a_{8} \left(\alpha^{2} + \gamma^{2}\right)}{6 \sqrt{\alpha^{4} + 2 \alpha^{2} \gamma^{2} + \gamma^{4}}} + \frac{\sqrt{6} \gamma^{2} a_{1} a_{4}}{6 \sqrt{\alpha^{2} + \gamma^{2}}} - \frac{a_{6} \left(4 \alpha^{2} + 4 \gamma^{2} + \pi^{2}\right)}{4 Re}$

$\displaystyle \frac{\sqrt{6} \pi \alpha \gamma a_{1} a_{4}}{3 \sqrt{4 \alpha^{4} + 8 \alpha^{2} \gamma^{2} + \pi^{2} \alpha^{2} + 4 \gamma^{4} + \pi^{2} \gamma^{2}}} + \frac{\sqrt{6} \gamma^{2} a_{2} a_{3} \left(12 \alpha^{2} + 12 \gamma^{2} + \pi^{2}\right)}{6 \sqrt{16 \alpha^{4} \gamma^{2} + 4 \pi^{2} \alpha^{4} + 32 \alpha^{2} \gamma^{4} + 12 \pi^{2} \alpha^{2} \gamma^{2} + \pi^{4} \alpha^{2} + 16 \gamma^{6} + 8 \pi^{2} \gamma^{4} + \pi^{4} \gamma^{2}}} - \frac{\sqrt{6} \pi^{2} \gamma^{2} a_{2} a_{3}}{3 \sqrt{16 \alpha^{4} \gamma^{2} + 4 \pi^{2} \alpha^{4} + 32 \alpha^{2} \gamma^{4} + 12 \pi^{2} \alpha^{2} \gamma^{2} + \pi^{4} \alpha^{2} + 16 \gamma^{6} + 8 \pi^{2} \gamma^{4} + \pi^{4} \gamma^{2}}} - \frac{a_{7} \left(4 \alpha^{2} + 4 \gamma^{2} + \pi^{2}\right)}{4 Re}$

$\displaystyle \frac{\sqrt{6} \pi \gamma a_{1} a_{2}}{2 \sqrt{4 \gamma^{2} + \pi^{2}}} - \frac{\sqrt{6} \pi \gamma a_{5} a_{7} \left(\alpha^{2} + \gamma^{2}\right)}{2 \sqrt{4 \alpha^{6} + 12 \alpha^{4} \gamma^{2} + \pi^{2} \alpha^{4} + 12 \alpha^{2} \gamma^{4} + 2 \pi^{2} \alpha^{2} \gamma^{2} + 4 \gamma^{6} + \pi^{2} \gamma^{4}}} - \frac{9 \pi^{2} a_{8}}{4 Re}$

# Create a numeric ODE
For further usage we take the numeric version of our ODEs.

For that we have to spesify integration volumne as well as the Reynolds-number

```python
with open("final_equations.pkl", "wb") as outf:
    pickle.dump(odes, outf)
```

```python
with open("final_equations.pkl","rb") as inf:
    odes = pickle.load(inf)
```

```python
L_x = 4*pi
L_z = 2*pi
re = 250
```

```python
class BUILD_EQUATION:

    def __init__(self, expressions, re, L_x, L_z):

        odes = []
        for index,exp in enumerate(expressions):
            N_exp = N(exp.subs({a:2*pi/L_x, g: 2*pi/L_z, Re: re}))
            f = lambdify(list_of_a,N_exp)
            odes.append(f)

        self.odes = odes
        self.result = np.zeros(9)

    def evaluation(self,set_of_a, t):

        for index, ode in enumerate(self.odes):

            self.result[index] = ode(*set_of_a)

        return self.result

equation_system = BUILD_EQUATION(odes,re,L_x,L_z)
ode = equation_system.evaluation
```

```python
from scipy.integrate import odeint

def get_IC_9MM():
    """ get random inital condition "close" to the attractor """
    a0 = np.random.random(9)-.5
    a0 /= 2
    return a0

time_array = np.linspace(0,1200,1000)
solution = odeint(ode,get_IC_9MM(),time_array)
```

```python
plt.figure(figsize=(10,5))
plt.plot(solution)
plt.grid()
plt.ylabel("Amplitude")
plt.xlabel("Time")
plt.savefig("img/solution.png",dpi=100)
plt.show()
```

```python
odes
```

    [sqrt(6)*pi*\gamma*a1*a2/(2*sqrt(4*\gamma**2 + pi**2)) - sqrt(6)*pi*\gamma*a5*a7*(\alpha**2 + \gamma**2)/(2*sqrt(4*\alpha**6 + 12*\alpha**4*\gamma**2 + pi**2*\alpha**4 + 12*\alpha**2*\gamma**4 + 2*pi**2*\alpha**2*\gamma**2 + 4*\gamma**6 + pi**2*\gamma**4)) - pi**2*a0/(4*Re) + pi**2/(4*Re),
     -sqrt(6)*pi*\alpha*\gamma*a4*a7/(6*sqrt(4*\alpha**4 + 8*\alpha**2*\gamma**2 + pi**2*\alpha**2 + 4*\gamma**4 + pi**2*\gamma**2)) + 5*sqrt(6)*\gamma**2*a3*a5/(9*sqrt(\alpha**2 + \gamma**2)) - sqrt(6)*\gamma**2*a4*a6/(6*sqrt(\alpha**2 + \gamma**2)) - sqrt(6)*pi*\gamma*a0*a2/(2*sqrt(4*\gamma**2 + pi**2)) - sqrt(6)*pi*\gamma*a2*a8/(2*sqrt(4*\gamma**2 + pi**2)) - a1*(3*\gamma**2 + pi**2)/(3*Re),
     sqrt(6)*pi*\alpha*\gamma*a3*a6/(3*sqrt(\alpha**2 + \gamma**2)*sqrt(4*\gamma**2 + pi**2)) + sqrt(6)*pi*\alpha*\gamma*a4*a5/(3*sqrt(\alpha**2 + \gamma**2)*sqrt(4*\gamma**2 + pi**2)) - sqrt(6)*\gamma**2*a3*a7*(12*\alpha**2 + 12*\gamma**2 + pi**2)/(6*sqrt(16*\alpha**4*\gamma**2 + 4*pi**2*\alpha**4 + 32*\alpha**2*\gamma**4 + 12*pi**2*\alpha**2*\gamma**2 + pi**4*\alpha**2 + 16*\gamma**6 + 8*pi**2*\gamma**4 + pi**4*\gamma**2)) + sqrt(6)*pi**2*a3*a7*(4*\alpha**2 + 8*\gamma**2/3)/(8*sqrt(16*\alpha**4*\gamma**2 + 4*pi**2*\alpha**4 + 32*\alpha**2*\gamma**4 + 12*pi**2*\alpha**2*\gamma**2 + pi**4*\alpha**2 + 16*\gamma**6 + 8*pi**2*\gamma**4 + pi**4*\gamma**2)) - a2*(4*\gamma**2 + pi**2)/(4*Re),
     -5*sqrt(6)*\alpha**2*a1*a5/(9*sqrt(\alpha**2 + \gamma**2)) - sqrt(6)*pi*\alpha*\gamma*a2*a6/(2*sqrt(\alpha**2 + \gamma**2)*sqrt(4*\gamma**2 + pi**2)) - sqrt(6)*\alpha*a0*a4/6 - sqrt(6)*\alpha*a4*a8/6 + sqrt(6)*pi**2*\gamma**2*a2*a7/(3*sqrt(16*\alpha**4*\gamma**2 + 4*pi**2*\alpha**4 + 32*\alpha**2*\gamma**4 + 12*pi**2*\alpha**2*\gamma**2 + pi**4*\alpha**2 + 16*\gamma**6 + 8*pi**2*\gamma**4 + pi**4*\gamma**2)) - sqrt(6)*pi**2*a2*a7*(4*\alpha**2 + 8*\gamma**2/3)/(8*sqrt(16*\alpha**4*\gamma**2 + 4*pi**2*\alpha**4 + 32*\alpha**2*\gamma**4 + 12*pi**2*\alpha**2*\gamma**2 + pi**4*\alpha**2 + 16*\gamma**6 + 8*pi**2*\gamma**4 + pi**4*\gamma**2)) - a3*(3*\alpha**2 + pi**2)/(3*Re),
     sqrt(6)*\alpha**2*a1*a6/(6*sqrt(\alpha**2 + \gamma**2)) - sqrt(6)*pi*\alpha*\gamma*a1*a7/(6*sqrt(4*\alpha**4 + 8*\alpha**2*\gamma**2 + pi**2*\alpha**2 + 4*\gamma**4 + pi**2*\gamma**2)) + sqrt(6)*pi*\alpha*\gamma*a2*a5/(3*sqrt(\alpha**2 + \gamma**2)*sqrt(4*\gamma**2 + pi**2)) + sqrt(6)*\alpha*a0*a3/6 + sqrt(6)*\alpha*a3*a8/6 - a4*(4*\alpha**2 + pi**2)/(4*Re),
     5*sqrt(6)*\alpha**2*a1*a3/(9*sqrt(\alpha**2 + \gamma**2)) - 2*sqrt(6)*pi*\alpha*\gamma*a2*a4/(3*sqrt(\alpha**2 + \gamma**2)*sqrt(4*\gamma**2 + pi**2)) + sqrt(6)*\alpha*a0*a6*(\alpha**2 + \gamma**2)/(6*sqrt(\alpha**4 + 2*\alpha**2*\gamma**2 + \gamma**4)) + sqrt(6)*\alpha*a6*a8*(\alpha**2 + \gamma**2)/(6*sqrt(\alpha**4 + 2*\alpha**2*\gamma**2 + \gamma**4)) - 5*sqrt(6)*\gamma**2*a1*a3/(9*sqrt(\alpha**2 + \gamma**2)) + sqrt(6)*pi*\gamma*a0*a7*(\alpha**2 + \gamma**2)/(2*sqrt(4*\alpha**6 + 12*\alpha**4*\gamma**2 + pi**2*\alpha**4 + 12*\alpha**2*\gamma**4 + 2*pi**2*\alpha**2*\gamma**2 + 4*\gamma**6 + pi**2*\gamma**4)) + sqrt(6)*pi*\gamma*a7*a8*(\alpha**2 + \gamma**2)/(2*sqrt(4*\alpha**6 + 12*\alpha**4*\gamma**2 + pi**2*\alpha**4 + 12*\alpha**2*\gamma**4 + 2*pi**2*\alpha**2*\gamma**2 + 4*\gamma**6 + pi**2*\gamma**4)) - a5*(\alpha**2 + \gamma**2)*(3*\alpha**2 + 3*\gamma**2 + pi**2)/(3*Re*sqrt(\alpha**4 + 2*\alpha**2*\gamma**2 + \gamma**4)),
     -sqrt(6)*\alpha**2*a1*a4/(6*sqrt(\alpha**2 + \gamma**2)) + sqrt(6)*pi*\alpha*\gamma*a2*a3/(6*sqrt(\alpha**2 + \gamma**2)*sqrt(4*\gamma**2 + pi**2)) - sqrt(6)*\alpha*a0*a5*(\alpha**2 + \gamma**2)/(6*sqrt(\alpha**4 + 2*\alpha**2*\gamma**2 + \gamma**4)) - sqrt(6)*\alpha*a5*a8*(\alpha**2 + \gamma**2)/(6*sqrt(\alpha**4 + 2*\alpha**2*\gamma**2 + \gamma**4)) + sqrt(6)*\gamma**2*a1*a4/(6*sqrt(\alpha**2 + \gamma**2)) - a6*(4*\alpha**2 + 4*\gamma**2 + pi**2)/(4*Re),
     sqrt(6)*pi*\alpha*\gamma*a1*a4/(3*sqrt(4*\alpha**4 + 8*\alpha**2*\gamma**2 + pi**2*\alpha**2 + 4*\gamma**4 + pi**2*\gamma**2)) + sqrt(6)*\gamma**2*a2*a3*(12*\alpha**2 + 12*\gamma**2 + pi**2)/(6*sqrt(16*\alpha**4*\gamma**2 + 4*pi**2*\alpha**4 + 32*\alpha**2*\gamma**4 + 12*pi**2*\alpha**2*\gamma**2 + pi**4*\alpha**2 + 16*\gamma**6 + 8*pi**2*\gamma**4 + pi**4*\gamma**2)) - sqrt(6)*pi**2*\gamma**2*a2*a3/(3*sqrt(16*\alpha**4*\gamma**2 + 4*pi**2*\alpha**4 + 32*\alpha**2*\gamma**4 + 12*pi**2*\alpha**2*\gamma**2 + pi**4*\alpha**2 + 16*\gamma**6 + 8*pi**2*\gamma**4 + pi**4*\gamma**2)) - a7*(4*\alpha**2 + 4*\gamma**2 + pi**2)/(4*Re),
     sqrt(6)*pi*\gamma*a1*a2/(2*sqrt(4*\gamma**2 + pi**2)) - sqrt(6)*pi*\gamma*a5*a7*(\alpha**2 + \gamma**2)/(2*sqrt(4*\alpha**6 + 12*\alpha**4*\gamma**2 + pi**2*\alpha**4 + 12*\alpha**2*\gamma**4 + 2*pi**2*\alpha**2*\gamma**2 + 4*\gamma**6 + pi**2*\gamma**4)) - 9*pi**2*a8/(4*Re)]

```python

```


# Image of a Black Hole
VANEL Mélina /
January 2021

## Photons Geodesics

First of all, the trajectory of photons is studied. The Black hole has a mass M, it is a non rotative one and not charged one, called : Schwarzchild Black hole. In polar coordinate one can express geodesic equation as :
 \begin{align}  \left(\frac{1}{r^2}\frac{\partial r}{\partial \theta} \right)^2 +\frac{1}{r^2}\left(1-\frac{r_s}{r} \right) = \frac{1}{b^2}\end{align}
 Where b is the infinite impact parameter and $r_s = 2GM/c^2$ the Schwarzschild radius.

This equation can be re written using $u=r_s/r$ and $u_c = b_c/b$ with  $b_c = 3\sqrt(3)/2$ :

 \begin{align}  \left(\frac{\partial u}{\partial \theta}\right)^2 + V(u) = 0   \end{align}

With \begin{align} V(u) = u^2(1-u)-\frac{4}{27}u_c^2 \end{align}

```python
# Packages
import numpy as np
import matplotlib.pyplot as plt
from scipy import *
from pylab import *
from scipy.integrate import odeint
from scipy.interpolate import interp1d
from matplotlib import ticker,cm

###########################
# Constants
###########################

G = 6.67*10**(-11)                  # Gravitation constant
Msoleil = 1.989*10**(30)            # Solar mass
M = 30*Msoleil                      # We consider a 30 solar mass black hole
c = 3.00*10**8                      # Speed light

rs = 2*G*M/c**2                     # Schwarzschild radius
bc = 3*np.sqrt(3)*rs/2

```

It is then possible to plot V(u) for different values of b the impact parameter.

```python
############################
# V(u) Definition
############################

def V(u,b):
    uc = bc/b
    return u**2*(1-u)-4/27*uc**2

def const(u):
    return 0*u

# Plotting V(u)
b=[0.8*bc,0.6*bc,0.9*bc,bc,1.3*bc,1.6*bc,2*bc,3*bc]
u = np.linspace(-1,2,2000)
for elem in b:
    plt.plot(u,V(u,elem),label='b='+str(elem/bc)+'bc')

plt.plot(u,const(u),'k')
plt.xlabel('u = rs/r')
plt.ylabel('V(u)')
plt.ylim([-0.5,0.2])
plt.xlim([0,1.5])

plt.legend()
plt.show()

```

It is possible to distinguish 2 different regimes :
- One for $b < b_c$, the arriving photon goes nearer than the limit trapping radius of the black hole. In this case, the arriving photons are trapped by the black hole.
- On the other hand, for $ b > b_c $, the arriving photons are facing a potential barrier and are defleted by the black hole and go back to infinity.

In order to find the photon geodesics, the differential equation needs to be solved. It is easier to work with the derived equation
\begin{align} u'' = 3/2u^2-u \end{align}

In order to solve this equation, the 4th order Runge Kutta method for second derivative differential equation is computed. The method is the following, if the equation to solve is $ u''=f(\theta,u,u') $ with $ f(\theta,u,u') = \frac{3}{2}u^2-u $. We then define :
$$ k_1 = f(\theta_n,u_n,u'_n) $$
$$ k_2 = f(\theta_n +\frac{h}{2},u_n+\frac{h}{2}u'_n,u'_n+\frac{h}{2}k_1) $$
$$ k_3 = f(\theta_n +\frac{h}{2},u_n+\frac{h}{2}u'_n+\frac{h^2}{4}k_1,u'_n+\frac{h}{2}k_2)$$
$$ k_4 = f(\theta_n +h,u_n+h*u'_n+\frac{h^2}{2}k_2,u'_n+h*k_3) $$

And then the solution will be deduced by :
$$ u_{n+1} = u_n + h*u'_n + \frac{h^2}{6}(k_1+k_2+k_3) $$
and
$$ u'_{n+1} = u'_n + \frac{h}{6}(k_1+2k_2+2k_3+k_4) $$

For the resolution we consider that if a photon is deviated by the black hole at the point that it goes back to infinity we consider that it stayes at infinity and don't go back to the black hole (*).

```python
# Function definition
def fonct(U):
    dudtheta = 3/2*U**2-U
    return dudtheta
```

```python
# Parameter to define theta start and theta end in order to solve the equation between theta start and stop
start = 0
stop = 20
N=1000
```

```python
def solve_equadiff(b):
    '''
    Solve differential equation u''=3/2u**2-u using 4th order Runge Kutta method for 2nd order differential equations
    Input : - b = impact parameter
    Output : ulist, uprimelist, theta
            - ulist = list of values of function u
            - uprimelist = list of values of derivative of u function
            - theta = list of theta on which the differential equation is solved
    '''

    uc = bc/b

    # Initialisation
    u0 = 0
    uprime0 = 2*uc/(3*np.sqrt(3))

    # Step
    h = (-start+stop)/(N-1)
    u=u0
    uprime=uprime0
    theta=np.linspace(start,stop,N)

    # Creation of ulist and uprimelist
    u_list=[]
    uprime_list=[]
    u_list.append(u)
    uprime_list.append(uprime)

    for i in range(len(theta)-1):
        k1 = fonct(u)
        k2 = fonct(u+h/2*uprime)
        k3 = fonct(u+h/2*uprime+h**2/4*k1)
        k4 = fonct(u+h*uprime+h**2/2*k2)

        u = u + h*uprime + h**2/6*(k1+k2+k3)
        uprime = uprime + h/6*(k1+2*k2+2*k3+k4)

        u_list.append(u)
        uprime_list.append(uprime)

    # when a photo goes back to infinity it stayes at infinity (*)
    t=0
    while (  u_list[t]>=0 and t<len(u_list)-1):
        t=t+1

    for k in range(t,len(u_list)):
            u_list[k] = 0

    # we set the maximum value for u to u = rs / r = 10
    t=0
    while (  u_list[t]<=10 and t<len(u_list)-1):
        t=t+1
    for k in range(t,len(u_list)):
            u_list[k] = 10
    return u_list, uprime_list, theta
```

It is then possible to plot u the solution of the differential equation for different values of b and to determine how many times the photon turns around the Black hole by looking at the support of the u function that is to say the length of the segment such as { $ \theta | u>0 $ }.

One can highlight the fact that the more b is close to bc te more the photon will turn around the Black hole.

```python
ul1 , upl, th = solve_equadiff(bc)
ul2 , upl, th = solve_equadiff(1.2*bc)
ul3 , upl, th = solve_equadiff(1.5*bc)
ul4 , upl, th = solve_equadiff(2*bc)
ul5 , upl, th = solve_equadiff(10**(10)*bc)

plt.figure()
#plt.plot(th[1],ul1,label='b/bc = {}'.format(str(1)))
plt.plot(th,ul2,label='b/bc = {}'.format(str(1.2)))
plt.plot(th,ul3,label='b/bc = {}'.format(str(1.5)))
plt.plot(th,ul4,label='b/bc = {}'.format(str(2)))
plt.plot(th,ul5,label='b/bc = Inf')
plt.ylim([0,0.5])
plt.xlabel('Theta')
plt.ylabel('u')
plt.title('u(theta)')
plt.legend()
plt.show()

```

One can compute now the phase portrait of the u fonction

```python
plt.figure()
b=[0.2*bc,0.6*bc,1*bc,3*bc]

for i in range(len(b)):
    ulist, uprimelist,theta = solve_equadiff(b[i])

    plt.plot(ulist,uprimelist,label='b='+str(b[i]/bc)+'bc')

plt.xlim([0,2])
plt.ylim([0,3])
plt.title('Phase portrait',loc='center')
plt.ylabel('u')
plt.ylabel('du/dtheta')
plt.legend()
plt.show()

```

Looking at the previous graph one can validate the fact that for $b<b_c$ the photon will fall on the Black hole, on the other hand for $b>b_c$ the photon are attracted and the goes bah to infinty with a null speed.

## Accretion Disk

Now that we know how photons propagate we will try to display images of a black hole and its properties.

```python
from IPython.display import Image
Image("disque_enonce.png")
```

We consider a black hole inclined with an angle i compared to the observer. The reverse return of light is valid for Schwaerzchild black hole. Therefore it is possible to goes back from the observer plan, follow back geodesics until finding the black hole disk at radius $r_d$.

In order to simplify we will assume that the unit vector nomal to the accretion disk plan is included in the plan $\alpha=\frac{\pi}{2}$.

A photon leaves the disk at a radius $r_d$ and angle $\theta_d$.

Three dimensional trigonometry gives us :
$$ cos(\theta_d) = \frac{-sin(\alpha)cos(i)}{\sqrt{1-cos^2(\alpha)cos^2(i))}} $$
and as we know that the photons are following the geodesics from the moment they leave the disk to the moment they arrive on observer plan one can write :
$$ r_d=u(\theta_d) $$
with u defined earlier.

In order to consider the computed quantities ( iso-radius, redshift, flux ) we introduces 2 variables x and y defined as following :
$$ x=b*cos(\alpha) $$ and $$ y=b*sin(\alpha) $$

```python
# Parameter to compute the accretion disk
pas = 100
i_list = [ 10*np.pi/180, 40*np.pi/180, 80*np.pi/180]
```

We first focus on the primary images, that is to say the images made by photons that didn't made a round around the black hole.

```python
def compute_primary(x, y):
    '''
    Fonction in order to compute the photons from primary image, direct photons coming
    '''
    x=x*bc
    y=y*bc

    b = np.sqrt(x**2+y**2)
    alpha = (y/b)/(np.abs(y/b))*np.arccos(x/b)

    thetad = np.arccos( -np.sin(alpha)*np.cos(i)/np.sqrt(1-np.cos(alpha)**2*np.cos(i)**2) )

    if len(thetad)>1:
        fct_u = np.ones((pas,pas))
        for k in range(pas):
            for l in range(pas):

                ul , upl, th = solve_equadiff(b[k][l])
                fct_u[k][l] = abs(1/(interp1d(th,ul,kind='cubic')(thetad[k][l])))

    else :

        fct_u = interp1d(th,ul,kind='cubic')(theta)
    return fct_u

```

```python
# Plot the map for different inclinaision ( 10, 40, 80 )
for a in range(len(i_list)) :
    i = i_list[a]
    x = np.linspace(-5,5,pas)
    y = np.linspace(-5,5,pas)
    X,Y = np.meshgrid(x, y)
    rd = compute_primary(X,Y)

    fig, ax = plt.subplots()

    plt.pcolor(X, Y, rd, cmap = 'inferno')
    #cset2 = ax.contour(X, Y, rd, colors='k')
    #cs = ax.contourf(X, Y, rd, locator=ticker.LogLocator(), cmap=cm.PuBu_r)
    level = [2,6,10,15,20,30,50]
    cs = ax.contourf(X, Y, rd,level)
    cbar = fig.colorbar(cs)
    cbar.ax.set_ylabel('rd/rs')
    plt.ylabel(' y ')
    plt.xlabel(' x ')
    plt.title('rd computation, inclinaison : {}'.format(str(i*180/np.pi)))

    plt.show()
```

- For low disk inclinaision ( 10 degree ) the rays are distorded above the black hole. We can make the hypothesis that these photon come from behind the black hole because of distorded images.
- For higher inclianision ( 80 degree ) iso-radius look like circular and non distorded. This confirm what is said in the previous point.

It is also to compute the secondary images of the black hole, obtained by replacing $\alpha$ by $\alpha + \pi$, that is to says $\theta_d = \pi + arccos \left (\frac{sin(\alpha)cos(i)}{\sqrt{1-cos^2(\alpha)cos^2(i))}} \right )$ but it won't be done here.

The emitted flux by disk surface unity at a distance r from the black hole is :

\begin{align}
    \dfrac{F_{emis}(x)}{F_0} = \dfrac{x^{-5/2}}{x - 3/2} \left[ \sqrt{x} - \sqrt{3} + \sqrt{\dfrac{3}{8}} \  ln \left ( \dfrac{\sqrt{x} + \sqrt{3/2}}{\sqrt{x} - \sqrt{3/2}} \dfrac{\sqrt{2} - 1}{\sqrt{2} + 1} \right )\right ]
\end{align}
with $x=r/r_s$

## Accretion disk luminosity

```python
def Femis_F0(x):
    facteur = x**(-5/2)/(x-3/2)
    quotient1 =(np.sqrt(x)+np.sqrt(3/2))/(np.sqrt(x)-np.sqrt(3/2))
    quotient2 = (np.sqrt(2)-1)/(np.sqrt(2)+1)
    return facteur*(np.sqrt(x)-np.sqrt(3)+np.sqrt(3/8)*np.log(quotient1*quotient2))

x_1 = np.linspace(0,5,1000)
x_l = np.linspace(0.1,25,1000)
plt.semilogy(x_l,Femis_F0(x_l))
plt.xlabel('r/rs')
plt.ylabel('log (Femis/F0)')
plt.show()

plt.plot(x_1,Femis_F0(x_1))
plt.xlabel('r/rs')
plt.ylabel('Femis/F0')
plt.ylim([-1,60])
plt.show()
```

#### Redshift

We then compute the redshift map around the black hole. As its density is very high the redshift around the black hole is modified. We have :
\begin{align}
1+z = \frac{1}{\sqrt{1-\frac{3}{2x}}}* \left [ 1 + \left ( \dfrac{3}{2x} \right )^{3/2} \dfrac{b}{b_c}\right ]cos(i)cos(\alpha)
\end{align}
The considered redshift map will be computed for a disk between $r_{min}=3r_s$ and $r_{max} >> r_{min}$.

```python
def comparex_3(x,min_x,max_x):
   '''
   Fonction in order to consider only a disk starting at 3rs
   '''
    bool_l = np.ones((len(x[0]), len(x[0])))
    for k in range(len(x[0])):
        for l in range(len(x[0])):
            if x[k][l] < min_x :
               bool_l[k][l] = 10**(20)
            if x[k][l] > max_x :
               bool_l[k][l]  = 10**(20)
    return bool_l

```

```python
def compute_zbis(x1,y,rad):
    '''
    Fonction in order to compute redshift from formula
    '''
    x1=x1*bc
    y=y*bc

    b = np.sqrt(x1**2+y**2)
    alpha = (y/b)/(np.abs(y/b))*np.arccos(x1/b)

    x = rad

    facteur1 = 1/np.sqrt(1-3/(2*x))
    facteur2 = ( 1 + (3/(2*x))**(3/2)*b/bc*np.cos(i)*np.cos(alpha))

    return comparex_3(x,rmin,rmax)*(facteur1*facteur2-1)

```

```python
rmin = 3
rmax = 25

# Plot the map for different inclinaision ( 10, 40, 80 )
for a in range(len(i_list)) :
    i = i_list[a]
    x = np.linspace(-3,3,pas)
    y = np.linspace(-3,3,pas)
    X,Y = np.meshgrid(x, y)
    rd = compute_primary(X,Y)
    zbis = compute_zbis(X,Y,rd)

    fig, ax = plt.subplots()
    plt.pcolor(X, Y, zbis, cmap = 'inferno')
    #cset2 = ax.contour(X, Y, zbis, colors='k')
    #cs = ax.contourf(X, Y, rd, locator=ticker.LogLocator(), cmap=cm.PuBu_r)
    level = [-0.25,-0.1,0,0.05,0.10,0.15,0.25,0.5,0.75]
    cs = ax.contourf(X, Y, zbis,level)
    cbar = fig.colorbar(cs)

    cbar.ax.set_ylabel('z')
    plt.ylabel(' y ')
    plt.xlabel('x')
    plt.title('z computation, inclinaison : {}'.format(str(i*180/np.pi)))

    plt.show()

```

- There is a huge difference between low inclinaision ( 10 degree ) where a redshift can be spotted on the right and a blueshift ($z<0$) on the left due to rotation of the accretion disk ( Doppler shift due to rotation ). On the other hand for high inclinasion ( 80 degree ) only redshift can be spotted.
- Redshift values are high above blueshift values. We will then assume that the distorsion induced by the disk rotation can be considered as a little pertubation compared to the distorsion induced by the gravitationnal field.

When the redshift is positive on the redshift map, the flux will decrease meanwhile when the redshift is negative the flux will increase which is coherent with the formula $F_{obs} = \frac{F_{emis}}{(1+z)^4} $. Thanks to this formula it is then possible to compute the flux.

```python

pas = 200
i_list=[10*np.pi/180,40*np.pi/180,80*np.pi/180]
# Comme image visible fonction de x et y
def Fobsbis(rd,z):

    Femis = np.ones((pas,pas))*0

    #F0 = 3/4*debm*c**2/(4*np.pi*rs**2)
    #thetad = np.arccos( -np.sin(alpha)*np.cos(i)/np.sqrt(1-np.cos(alpha)**2*np.cos(i)**2) )
    for k in range(pas):
        for l in range(pas):
            if not math.isnan(rd[k][l]):
                if rd[k][l] >= rmin-3 and rd[k][l] <= rmax +150  :
                    facteur = rd[k][l]**(-5/2)/(rd[k][l]-3/2)
                    quotient1 =(np.sqrt(rd[k][l])+np.sqrt(3/2))/(np.sqrt(rd[k][l])-np.sqrt(3/2))
                    quotient2 = (np.sqrt(2)-1)/(np.sqrt(2)+1)
                    Femis[k][l] = (facteur*(np.sqrt(rd[k][l])-np.sqrt(3)+np.sqrt(3/8)*np.log(quotient1*quotient2)))/(z[k][l]+1)**4

    Femis = Femis.astype('float64')
    NAN = np.argwhere(np.isnan(Femis))
    for elem in NAN :
        Femis[elem[0]][elem[1]]=0

    return Femis

```

We now obtain an numerical image of a black hole.

```python
for a in range(len(i_list)) :
    i = i_list[a]
    x = np.linspace(-7,7,pas)
    y = np.linspace(-7,7,pas)
    X,Y = np.meshgrid(x, y)

    rd = compute_primary(X,Y)
    zbis = compute_zbis(X,Y,rd)
    F = Fobsbis(rd,zbis)
    fig, ax = plt.subplots()
    plt.pcolor(X, Y, F,cmap = 'gist_heat')
    #level = [0.05,0.10,0.15,0.2,0.3,0.5,0.6,0.9,1.2,1.5,1.8,2.1]*10**(-3)
    #cs = ax.contourf(X, Y, F,level)
    #cset2 = ax.contour(X, Y, F, colors='k')
    #cs = ax.contourf(X, Y, F, locator=ticker.LogLocator(), cmap=cm.Wistia)
    #cbar = fig.colorbar(cs)
    cbar.ax.set_ylabel('Fobs/F0')
    plt.ylabel(' y ')
    plt.xlabel('x')
    plt.title('F/F0 computation, inclinaison : {}'.format(str(i*180/np.pi)))

    plt.show()

```


# Before we begin

* Github (Github Education)
* Bitbucket
* Kaggle

# Introduction
In this week, we want to implement an Ant Colony Optimization Algorithm to solve Travelling Sales Man problem.

```python
import random
import math
import operator
import matplotlib.pyplot as plt
```

# Content
* Travelling Sales Man Problem
* Helper Functions
* Cost & Pheromone Graph
* Designing Ants
* Designing ACO
* Running

# Travelling Sales Man Problem(TSP)

The travelling salesman problem (TSP) asks the following question: "Given a list of cities and the distances between each pair of cities, what is the shortest possible route that visits each city and returns to the origin city?" It is an NP-hard problem in combinatorial optimization, important in operations research and theoretical computer science.

# Helper Functions

\begin{align}
Distance = \sqrt{ (y_2 - y_1)^2 + (x_2 - x_1)^2}
\end{align}

```python
def distance(city1: dict, city2: dict):
    return math.sqrt((city1['x'] - city2['x']) ** 2 + (city1['y'] - city2['y']) ** 2)
```

```python
def plot(points, path: list):
    x = []
    y = []
    for point in points:
        x.append(point[0])
        y.append(point[1])

    y = list(map(operator.sub, [max(y) for i in range(len(points))], y)) # for better visualization
    plt.plot(x, y, 'co')

    for k in range(1, len(path)):
        i = path[k - 1] # index of first city
        j = path[k] # index of next city

        plt.arrow(x[i], y[i], x[j] - x[i], y[j] - y[i], color='r', length_includes_head=True)

    plt.xlim(0, max(x) * 1.1)

    plt.ylim(0, max(y) * 1.1)

    plt.show()
```

# Prerequisites

### ACO Algorihtm

### Strategy

With $Q \in [0,1]$
### Rho
\begin{align}
T_{ij}(t) \leftarrow rho * T_{ij}(t)
\end{align}
With $rho \in [0,1]$
### Transition Probability

With $\alpha, \beta \in [0,1]$

# Cost & Pheromone Graph

The graph is a data structure that has the matrices that we needed to evaluate transition probability:

```python
class Graph(object):
    def __init__(self, cost_matrix: list, rank: int):
        """
        :param cost_matrix:
        :param rank: rank of the cost matrix
        """
        self.matrix = cost_matrix
        self.rank = rank
        # noinspection PyUnusedLocal
        self.pheromone = [[1 / (rank * rank) for j in range(rank)] for i in range(rank)]
```

# Designing Ants

```python
class Ant(object):
    def __init__(self, aco, graph: Graph):
        self.colony = aco
        self.graph = graph
        self.total_cost = 0.0
        self.path = []  # path
        self.pheromone_delta = []  # the local increase of pheromone
        self.allowed = [i for i in range(graph.rank)]  # nodes which are allowed for the next selection
        self.eta = [[0 if i == j else 1 / graph.matrix[i][j] for j in range(graph.rank)] \
                        for i in range(graph.rank)]  # heuristic information for calculating
        start = random.randint(0, graph.rank - 1)  # start from any node
        self.path.append(start)
        self.current = start
        self.allowed.remove(start)

    def select_next(self):
        denominator = 0
        for i in self.allowed:
            denominator += self.graph.pheromone[self.current][i] ** self.colony.alpha \
                * self.eta[self.current][i] ** self.colony.beta
        # noinspection PyUnusedLocal
        probabilities = [0 for i in range(self.graph.rank)]  # probabilities for moving to a node in the next step
        for i in range(self.graph.rank):
            try:
                self.allowed.index(i)  # test if allowed list contains i
                probabilities[i] = self.graph.pheromone[self.current][i] ** self.colony.alpha * \
                    self.eta[self.current][i] ** self.colony.beta / denominator
            except ValueError:
                pass  # do nothing

        # select next node by probability roulette
        selected = 0
        rand = random.random()
        for i, probability in enumerate(probabilities):
            rand -= probability
            if rand <= 0:
                selected = i
                break
        self.allowed.remove(selected)
        self.path.append(selected)
        self.total_cost += self.graph.matrix[self.current][selected]
        self.current = selected

    # noinspection PyUnusedLocal
    def update_pheromone_delta(self):
        self.pheromone_delta = [[0 for j in range(self.graph.rank)] for i in range(self.graph.rank)]
        for k in range(1, len(self.path)):
            i = self.path[k - 1]
            j = self.path[k]
            if self.colony.update_strategy == 1:  # ant-quality system
                self.pheromone_delta[i][j] = self.colony.Q
            elif self.colony.update_strategy == 2:  # ant-density system
                # noinspection PyTypeChecker
                self.pheromone_delta[i][j] = self.colony.Q / self.graph.matrix[i][j]
            else:  # ant-cycle system
                self.pheromone_delta[i][j] = self.colony.Q / self.total_cost
```

# Designing ACO

```python
class ACO(object):
    def __init__(self, ant_count: int, generations: int,
                 alpha: float, beta: float, rho: float,
                 q: int, strategy: int):
        """
        :param ant_count:
        :param generations:
        :param alpha: relative importance of pheromone
        :param beta: relative importance of heuristic information
        :param rho: pheromone residual coefficient
        :param q: pheromone intensity
        :param strategy: pheromone update strategy. 0 - ant-cycle, 1 - ant-quality, 2 - ant-density
        """
        self.Q = q
        self.rho = rho # Evapuration Rate
        self.beta = beta
        self.alpha = alpha
        self.ant_count = ant_count
        self.generations = generations
        self.update_strategy = strategy

    def _update_pheromone(self, graph: Graph, ants: list):
        for i, row in enumerate(graph.pheromone):
            for j, col in enumerate(row):
                graph.pheromone[i][j] *= self.rho # Evapuration
                for ant in ants:
                    graph.pheromone[i][j] += ant.pheromone_delta[i][j]

    def solve(self, graph: Graph):
        """
        :param graph:
        """
        best_cost = float('inf')
        best_solution = []
        for gen in range(self.generations):
            # noinspection PyUnusedLocal
            ants = [Ant(self, graph) for i in range(self.ant_count)]
            for ant in ants:
                for i in range(graph.rank - 1):
                    ant.select_next()
                ant.total_cost += graph.matrix[ant.path[-1]][ant.path[0]]
                if ant.total_cost < best_cost:
                    best_cost = ant.total_cost
                    best_solution = [] + ant.path
                # update pheromone
                ant.update_pheromone_delta()
            self._update_pheromone(graph, ants)
            print('generation #{}, best cost: {}, path: {}'.format(gen, best_cost, best_solution))
        return best_solution, best_cost
```

# Running

```python
def main():
    # Loading Data from files
    cities = []
    points = []
    with open('./data/chn31.txt') as f:
        for line in f.readlines():
            city = line.split(' ')
            cities.append(dict(index=int(city[0]), x=int(city[1]), y=int(city[2])))
            points.append((int(city[1]), int(city[2])))

    # Calculating Cost matrix => distance between city i and j
    cost_matrix = []
    rank = len(cities)
    for i in range(rank):
        row = []
        for j in range(rank):
            row.append(distance(cities[i], cities[j]))
        cost_matrix.append(row)

    # Instaniate ACO, and Run
    aco = ACO(10, 100, 1.0, 10.0, 0.5, 10, 2)
    graph = Graph(cost_matrix, rank)
    path, cost = aco.solve(graph)
    print('cost: {}, path: {}'.format(cost, path))

    # Ploting the best cycle found
    plot(points, path)

if __name__ == '__main__':
    main()
```

Week 5 - Ant Colony Optimization/Ant Colony.ipynb


# ***Introduction to Radar Using Python and MATLAB***
## Andy Harrison - Copyright (C) 2019 Artech House
<br/>

# Circular Array Antenna
***

The circular array is shown in Figure 3.22.  For this type of array, the radiating elements are placed on a circular path with spacing of $a \, \Delta\phi$, where $a$ is the radius, and $\Delta\phi$ is the angular difference between the elements.  The normalized electric field is written as

$$
	E(r, \theta, \phi) = \sum\limits_{n=1}^{N}a_n\, \frac{e^{-j k r_n}}{r_n} \hspace{0.5in} \text{(V/m)},
$$

where

$$
	r_n = \sqrt{r^2 + a^2 - 2\, a\, r\, \cos \psi_n} \hspace{0.5in} \text{(m)}.
$$

For phase terms, $r_n$ is approximated by

$$
	r_n \approx r - a\, \sin\theta\, \cos(\phi - \phi_n) \hspace{0.5in} \text{(m)},
$$

where $\phi_n$ is the angular position of the $n$th element, and is expressed as

\begin{equation}
	\phi_n = \frac{2\pi\, n}{N} \hspace{0.5in} \text{(rad)},
\end{equation}

and for amplitude $r_n \approx r$.  The coefficients, $a_n$, are represented by an amplitude and phase as

$$
	a_n = A_n\, e^{j \alpha_n},
$$

where $A_n$ is the amplitude and $\alpha_n$ is the phase.  The array factor for the circular array is now written as

$$
	{AF}(\theta, \phi) = \sum\limits_{n=1}^{N}A_n\, e^{j [k a \sin\theta\, \cos(\phi-\phi_n) + \alpha_n]} .
$$
***

Begin by getting the library path

```python
import lib_path
```

Set the operating frequency (Hz), the number of elements, the scan angle (theta - degrees), (phi - degrees), and the radius (m)

```python
frequency = 300e6

number_of_elements = 40.0

scan_angle_theta = 30.0

scan_angle_phi = 30.0

radius = 1.1
```

Set up the theta and phi arrays using the `meshgrid` and `linspace` routines from `scipy`

```python
from numpy import linspace, meshgrid, radians

from scipy.constants import pi

n = 360

m = int(n / 8)

theta, phi = meshgrid(linspace(0.0 * pi, 0.5 * pi, n), linspace(0.0, 2.0 * pi, n))
```

Set up the keyword args

```python
kwargs = {'number_of_elements': number_of_elements,

          'scan_angle_theta': radians(scan_angle_theta),

          'scan_angle_phi': radians(scan_angle_phi),

          'radius': radius,

          'frequency': frequency,

          'theta': theta,

          'phi': phi}
```

Calculate the array factor using the `circular_uniform` routines

```python
from Libs.antenna.array import circular_uniform

af = circular_uniform.array_factor(**kwargs)
```

Create the U-V coordinates for displaying the antenna pattern

```python
from numpy import sin, cos

uu = sin(theta) * cos(phi)

vv = sin(theta) * sin(phi)
```

Create the color plot of the array factor using the `matplotlib` routines

```python
from matplotlib import pyplot as plt

# Set the figure size

plt.rcParams["figure.figsize"] = (15, 10)

# Set up the axes

fig, axes1 = plt.subplots()

im = axes1.pcolor(uu, vv, abs(af), cmap="jet")

cbar = fig.colorbar(im, ax=axes1, orientation='vertical')

cbar.set_label("Normalized Electric Field (V/m)", size=10)

# Set the x- and y-axis labels

axes1.set_xlabel("U (sines)", size=12)

axes1.set_ylabel("V (sines)", size=12)

# Set the plot title

plt.title('Circular Array - Array Factor', size=14)
```

Create the contour plot using the `matplotlib` routines

```python
# Set the figure size

plt.rcParams["figure.figsize"] = (12, 12)

plt.contour(uu, vv, abs(af), 20, cmap="jet", vmin=-0.2, vmax=1.0)

plt.grid(linestyle=':', linewidth=0.5)

# Set the x- and y-axis labels

plt.xlabel("U (sines)", size=12)

plt.ylabel("V (sines)", size=12)

# Set the plot title

plt.title('Circular Array - Array Factor', size=14)
```

Create the line plot using the `matplotlib` routines

```python
from numpy import degrees, log10

plt.plot(degrees(theta[0]), 20.0 * log10(abs(af[m])), '', label='E Plane')

plt.plot(degrees(theta[0]), 20.0 * log10(abs(af[0])), '--', label='H Plane')

# Set the y axis limit

plt.ylim(-60, 5)

# Set the x and y axis labels

plt.xlabel("Theta (degrees)", size=12)

plt.ylabel("Array Factor (dB)", size=12)

# Turn on the grid

plt.grid(linestyle=':', linewidth=0.5)

# Place the legend

plt.legend(loc='upper right', prop={'size': 10})

# Set the plot title

plt.title('Circular Array - Array Factor', size=14)
```


```python
%matplotlib inline
```

# Wasserstein 1D with PyTorch

In this small example, we consider the following minization problem:

\begin{align}\mu^* = \min_\mu W(\mu,\nu)\end{align}

where $\nu$ is a reference 1D measure. The problem is handled
by a projected gradient descent method, where the gradient is computed
by pyTorch automatic differentiation. The projection on the simplex
ensures that the iterate will remain on the probability simplex.

This example illustrates both `wasserstein_1d` function and backend use within
the POT framework.

```python
# Author: Nicolas Courty <ncourty@irisa.fr>
#         Rémi Flamary <remi.flamary@polytechnique.edu>
#
# License: MIT License

import numpy as np
import matplotlib.pylab as pl
import matplotlib as mpl
import torch

from ot.lp import wasserstein_1d
from ot.datasets import make_1D_gauss as gauss
from ot.utils import proj_simplex

red = np.array(mpl.colors.to_rgb('red'))
blue = np.array(mpl.colors.to_rgb('blue'))

n = 100  # nb bins

# bin positions
x = np.arange(n, dtype=np.float64)

# Gaussian distributions
a = gauss(n, m=20, s=5)  # m= mean, s= std
b = gauss(n, m=60, s=10)

# enforce sum to one on the support
a = a / a.sum()
b = b / b.sum()

device = "cuda" if torch.cuda.is_available() else "cpu"

# use pyTorch for our data
x_torch = torch.tensor(x).to(device=device)
a_torch = torch.tensor(a).to(device=device).requires_grad_(True)
b_torch = torch.tensor(b).to(device=device)

lr = 1e-6
nb_iter_max = 800

loss_iter = []

pl.figure(1, figsize=(8, 4))
pl.plot(x, a, 'b', label='Source distribution')
pl.plot(x, b, 'r', label='Target distribution')

for i in range(nb_iter_max):
    # Compute the Wasserstein 1D with torch backend
    loss = wasserstein_1d(x_torch, x_torch, a_torch, b_torch, p=2)
    # record the corresponding loss value
    loss_iter.append(loss.clone().detach().cpu().numpy())
    loss.backward()

    # performs a step of projected gradient descent
    with torch.no_grad():
        grad = a_torch.grad
        a_torch -= a_torch.grad * lr  # step
        a_torch.grad.zero_()
        a_torch.data = proj_simplex(a_torch)  # projection onto the simplex

    # plot one curve every 10 iterations
    if i % 10 == 0:
        mix = float(i) / nb_iter_max
        pl.plot(x, a_torch.clone().detach().cpu().numpy(), c=(1 - mix) * blue + mix * red)

pl.legend()
pl.title('Distribution along the iterations of the projected gradient descent')
pl.show()

pl.figure(2)
pl.plot(range(nb_iter_max), loss_iter, lw=3)
pl.title('Evolution of the loss along iterations', fontsize=16)
pl.show()
```

## Wasserstein barycenter
In this example, we consider the following Wasserstein barycenter problem
$$ \\eta^* = \\min_\\eta\;\;\; (1-t)W(\\mu,\\eta) + tW(\\eta,\\nu)$$
where $\\mu$ and $\\nu$ are reference 1D measures, and $t$
is a parameter $\in [0,1]$. The problem is handled by a project gradient
descent method, where the gradient is computed by pyTorch automatic differentiation.
The projection on the simplex ensures that the iterate will remain on the
probability simplex.

This example illustrates both `wasserstein_1d` function and backend use within the
POT framework.

```python
device = "cuda" if torch.cuda.is_available() else "cpu"

# use pyTorch for our data
x_torch = torch.tensor(x).to(device=device)
a_torch = torch.tensor(a).to(device=device)
b_torch = torch.tensor(b).to(device=device)
bary_torch = torch.tensor((a + b).copy() / 2).to(device=device).requires_grad_(True)

lr = 1e-6
nb_iter_max = 2000

loss_iter = []

# instant of the interpolation
t = 0.5

for i in range(nb_iter_max):
    # Compute the Wasserstein 1D with torch backend
    loss = (1 - t) * wasserstein_1d(x_torch, x_torch, a_torch.detach(), bary_torch, p=2) + t * wasserstein_1d(x_torch, x_torch, b_torch, bary_torch, p=2)
    # record the corresponding loss value
    loss_iter.append(loss.clone().detach().cpu().numpy())
    loss.backward()

    # performs a step of projected gradient descent
    with torch.no_grad():
        grad = bary_torch.grad
        bary_torch -= bary_torch.grad * lr  # step
        bary_torch.grad.zero_()
        bary_torch.data = proj_simplex(bary_torch)  # projection onto the simplex

pl.figure(3, figsize=(8, 4))
pl.plot(x, a, 'b', label='Source distribution')
pl.plot(x, b, 'r', label='Target distribution')
pl.plot(x, bary_torch.clone().detach().cpu().numpy(), c='green', label='W barycenter')
pl.legend()
pl.title('Wasserstein barycenter computed by gradient descent')
pl.show()

pl.figure(4)
pl.plot(range(nb_iter_max), loss_iter, lw=3)
pl.title('Evolution of the loss along iterations', fontsize=16)
pl.show()
```


### Define orthogonal matrix in the exponential form:
$$R^{ij}=\exp\Big[\frac{1}{2} \sum\limits_{m,n}\theta_{m,n} J_{m,n}^{i,j} \Big]$$
with:
$$J_{m,n}^{ij}=  \delta_{mi}\delta_{nj}-\delta_{mj} \delta_{ni}, \, \theta_{m,n} =-\theta_{n,m} $$

*There seems to be no way to do the exponential analytically for arbitrary dimension*

###### In general, you can just use
$$ R^{ij}= e^{J^{i,j}} $$

with $J^{i,j}=-J^{j,i}$

```python
import sympy as sp
import numpy as np
```

```python
sp.init_printing()
```

```python
def KronD(i,j):
    if i==j:
        return 1
    else:
        return 0
```

```python
dim=3
theta=sp.IndexedBase('theta', shape=(dim,dim))
(m,n,i,j)=sp.symbols('m n j i', cls=sp.Idx)

theta=sp.Matrix(dim,dim, lambda i,j : theta[i+1,j+1] if i<j else 0 if i==j else -theta[j+1,i+1])
```

```python
tmpJ=[]
for m in range(1,1+dim):
    for n in range(1,1+dim):
        for i in range(1,1+dim):
            for j in range(1,1+dim):
                #tmpJ.append(J[m,n,i,j])
                tmpJ.append(KronD(m,i)*KronD(n,j)-KronD(m,j)*KronD(n,i) )

J=sp.Array(tmpJ,shape=(dim,dim,dim,dim))
```

```python
m,n=sp.symbols('m n ')

#ExpSum=sp.Matrix(dim,dim, lambda i,j: sp.Sum( sp.Symbol('theta'+str([m,n])) *sp.Symbol('J'+str([m,n,i,j])),(m, 0,dim-1),(n, 0,dim-1)))
ExpSum=sp.Matrix(dim,dim, lambda i,j: sp.summation(theta[m,n]/2*J[m,n,i,j],(m, 0,dim-1),(n, 0,dim-1)))

R=sp.exp(sp.Matrix(dim,dim, lambda i,j: sp.summation(theta[m,n]/2*J[m,n,i,j],(m, 0,dim-1),(n, 0,dim-1))))
```

```python
sp.simplify(R.rewrite(sp.sin))
```

```python
sp.simplify(np.dot(np.array(sp.transpose(R.rewrite(sp.cos))),np.array(R.rewrite(sp.cos))))
```

```python

```

```python
#Check numerically

dim=2
(m,n,i,j)=sp.symbols('m n j i', cls=sp.Idx)

theta=[[ np.random.rand() if i<j else 0   for j in range(dim)] for i in range(dim)]
theta=[[ theta[i][j] if i<j else -theta[j][i]    for j in range(dim)] for i in range(dim)]

tmpJ=[]
for m in range(1,1+dim):
    for n in range(1,1+dim):
        for i in range(1,1+dim):
            for j in range(1,1+dim):
                tmpJ.append(KronD(m,i)*KronD(n,j)-KronD(m,j)*KronD(n,i) )

J=np.reshape(tmpJ,(dim,dim,dim,dim))

ExpSum=[]
for i in range(dim):
    for j in range(dim):
        stmp=0
        for m in range(dim):
            for n in range(dim):
                stmp+=theta[m][n]*J[m,n,i,j]/2.
        ExpSum.append(stmp)
ExpSum=np.reshape(ExpSum,(dim,dim))

import scipy as scp
R=scp.linalg.expm(ExpSum)

Test=np.dot(R,np.transpose(R))

sp.Matrix(dim,dim , lambda i,j:Test[i][j] )
```

```python

```

```python

```

```python

```


## Creating a simple system with the discretizer

In addition to usual imports we need to make and work with a kwant system, we import the discretizer. Since the discretizer takes the symbolic Hamiltonian in the form of a sympy expression, we also need sympy. The momentum operators that we can use in the Hamiltonian are predefined and we also have to import those.

```python
import sympy
sympy.init_printing(use_latex='mathjax')

import kwant
from discretizer import Discretizer
from discretizer import momentum_operators
from discretizer import coordinates

from types import SimpleNamespace
import scipy.sparse.linalg as sla

from matplotlib import pyplot
%matplotlib inline
```

We write the kinetic energy in terms of the imported momentum operators. In addition we add a potential $V$.

```python
kx, ky, kz = momentum_operators
x, y, z = coordinates
V = sympy.Symbol('V')

H = kx**2 + ky**2 - V(x,y)
H
```

$$k_{x}^{2} + k_{y}^{2} - V{\left (x,y \right )}$$

We pass the symbolic Hamiltonian over to the Discretizer and declare that $V$ is space dependend. We do that with the full output. Take a look at the onsite and hopping functions the discretizer created!

```python
tb = Discretizer(H, verbose=True)
```

    Discrete coordinates set to:  ['x', 'y']

    Function generated for (0, 1):
    def _anonymous_func(site1, site2, p):
        (x, y, ) = site2.pos
        return (-1)

    Function generated for (1, 0):
    def _anonymous_func(site1, site2, p):
        (x, y, ) = site2.pos
        return (-1)

    Function generated for (0, 0):
    def _anonymous_func(site, p):
        (x, y, ) = site.pos
        V = p.V
        return (4 - V(x, y))

The discretizer can also give these functions in symbolic form, if you want to check them. Then it returns a dictionary where the key is the hopping kind and the entry is the symbolic function. The key of the onsite function is filled with zeroes, so in this case it is $(0, 0)$.

```python
tb.symbolic_hamiltonian
```

$$\left \{ \left ( 0, \quad 0\right ) : - V{\left (x,y \right )} + \frac{4}{a^{2}}, \quad \left ( 0, \quad 1\right ) : - \frac{1}{a^{2}}, \quad \left ( 1, \quad 0\right ) : - \frac{1}{a^{2}}\right \}$$

```python
def stadium(position):
    (x, y) = position
    x = max(abs(x) - 20, 0)
    return x**2 + y**2 < 30**2
```

From the discretizer we get a lattice, the onsite and the hopping functions, so we almost have all we need to make and use the system. We only need to fix the value of $V$. That is why we declare a simple namespace, in which we define $V$ and pass it over as 'args' argument when we call the system's Hamiltonian. Remember that we declared $V$ as space dependend, so we have to make it a function of the position variables $x$ and $y$.

```python
sys = kwant.Builder()
sys[tb.lattice.shape(stadium, (0, 0))] = tb.onsite

for hop, val in tb.hoppings.items():
    sys[hop] = val

sys = sys.finalized()

kwant.plot(sys)
pyplot.show()

par = SimpleNamespace(V=lambda x, y: 5e-6*(x**2 + y**2))
ham = sys.hamiltonian_submatrix(args=[par], sparse=True)
ev, vec = sla.eigsh(ham, k=10, which='SA')

kwant.plotter.map(sys, abs(vec[:,0])**2)
pyplot.show()
```

We could have done the same thing with less code as well. The discretizer's 'build' function can directly make the kwant system for you if you pass it a shape.

```python
del sys

sys = tb.build(stadium, (0,0))
sys = sys.finalized()

kwant.plot(sys)
pyplot.show()

par = SimpleNamespace(V=lambda x,y: 5e-6*(x**2 + y**2))
ham = sys.hamiltonian_submatrix(args=[par], sparse=True)
ev, vec = sla.eigsh(ham, k=10, which='SA')

kwant.plotter.map(sys, abs(vec[:,0])**2)
pyplot.show()
```


```python
# HIDDEN
from datascience import *
from prob140 import *
import numpy as np
import matplotlib.pyplot as plt
plt.style.use('fivethirtyeight')
%matplotlib inline
import math
from scipy import stats
from sympy import *
init_printing()
```

## Probabilities and Expectations ##

A function $f$ on the plane is called a *joint density* if:

- $f(x, y) \ge 0$ for all $x$, $y$
- $\int_x \int_y f(x, y)dydx = 1$

If you think of $f$ as a surface, then the first condition says that the surface is on or above the plane. The second condition says that the total volume under the surface is 1.

Think of probabilities as volumes under the surface, and define $f$ to be the *joint density of random variables $X$ and $Y$* if

$$
P((X, Y) \in A) ~ = ~ \mathop{\int \int}_A f(x,y)dydx ~~~~~ \text{for all } A
$$

That is, the chance that the random point $(X, Y)$ falls in the region $A$ is the volume under the joint density surface over the region $A$.

This is a two-dimensional analog of the fact that probabilities involving a single continuous random variable can be thought of as areas under the density curve.

### Infinitesimals ###
Also analogous is the interpretation of the joint density as part of the calculation of the probability of an infinitesimal region.

The infinitesimal region is a tiny rectangle in the plane just around the point $(x, y)$. Its width is $dx$ and its length is $dy$. The corresponding volume is that of a rectangular box whose base is the tiny rectangle and whose height is $f(x, y)$.

Thus for all $x$ and $y$,

$$
P(X \in dx, Y \in dy) ~ \sim ~ f(x, y)dxdy
$$

and the joint density measures *probability per unit area*:

$$
f(x, y) ~ \sim ~ \frac{P(X \in dx, Y \in dy)}{dxdy}
$$

An example will help us visualize all this. Let $f$ be defined as follows:

$$
f(x, y) ~ = ~
\begin{cases}
120x(y-x)(1-y), ~~~ 0 < x < y < 1 \\
0 ~~~~~~~~ \text{otherwise}
\end{cases}
$$

For now, just assume that this is a joint density, that is, it integrates to 1. Let's first take a look at what the surface looks like.

### Plotting the Surface ###
To do this, we will use a 3-dimensional plotting routine. First, we define the joint density function. For use in our plotting routine, this function must take $x$ and $y$ as its inputs and return the value $f(x, y)$ as defined above.

```python
def joint(x,y):
    if y < x:
        return 0
    else:
        return 120 * x * (y-x) * (1-y)
```

Now we will call `Plot_3d` to plot the surface. The arguments are the limits on the $x$ and $y$ axes, the name of the function to be plotted, and two optional arguments `rstride` and `cstride` that determine how many grid lines to use. Larger numbers correspond to less frequent grid lines.

```python
Plot_3d(x_limits=(0,1), y_limits=(0,1), f=joint, cstride=4, rstride=4)
```

The surface has level 0 in the lower right hand triangle because it is not possible for the point $(X, Y)$ to be in that region.

The possible values of $(X, Y)$ are the blue region shown below. For calculations by hand, we will frequently draw just the possible values and not the surface.

```python
# NO CODE

plt.figure(figsize=(3, 3))
plt.axes().set_aspect('equal')
plt.plot([0, 0], [0, 1], color='k', lw=2)
plt.plot([0, 1], [0, 1], color='k', lw=2)
plt.plot([0, 1], [1, 1], color='k', lw=2)
xx = np.arange(0, 1.11, 0.1)
yy = np.ones(len(xx))
plt.fill_between(xx, xx, yy, alpha=0.3, color='blue')
plt.xlim(-0.05, 1)
plt.ylim(0, 1.05)
plt.xticks(np.arange(0, 1.1, 0.25))
plt.yticks(np.arange(0, 1.1, 0.25))
plt.xlabel('$x$')
plt.ylabel('$y$', rotation=0)
plt.title('Possible Values of $(X, Y)$');
```

### The Total Volume Under the Surface ###
The function $f$ looks like a bit of a mess but it is easy to see that it is non-negative. It's a good idea to check that the total probability under the surface is equal to 1.

To set up the double integral over the entire region of possible values, notice that for each fixed value of $y$, the value of $x$ goes from 0 to $y$. The values of $y$ go from 0 to 1.

We will first fix $y$ and integrate with respect to $x$. Then we will integrate with respect to $y$. Thus the total integral is

$$
\begin{align*}
& ~~ \int_0^1 \int_0^y 120x(y-x)(1-y)dxdy \\
&= ~ 120 \int_0^1 (1-y) \Big{(}\int_0^y (xy - x^2)dx\Big{)}dy \\
&= ~ 20 \int_0^1 (1-y)y^3 dy ~ = ~  20 \big{(} \frac{1}{4} - \frac{1}{5}\big{)} ~ = ~  1
\end{align*}
$$

#### By `SymPy` ####
To use `SymPy` we must create two symbolic variables. As our variables are positive, we can specify that as well. Then we can assign the expression that defines the function to the name `f`. This specification doesn't say that $x < y$ but we will enforce that condition when we integrate.

```python
x = Symbol('x', positive=True)
y = Symbol('y', positive=True)

f = 120*x*(y-x)*(1-y)
f
```

Displaying the double integral requires a call to `Integral` that specifies the inner integral first and then the outer. The call says:

- The function being integrated is $f$.
- The inner integral is over the variable $x$ which goes from $0$ to y.
- The outer integral is over the variable $y$ which goes from 0 to 1.

```python
Integral(f, (x, 0, y), (y, 0, 1))
```

To evaluate the integral, use `doit()`:

```python
Integral(f, (x, 0, y), (y, 0, 1)).doit()
```

### Probabilities as Volumes ###
Probabilities are volumes under the joint density surface. In other words, they are double integrals of the function $f$. For each probability, we have to first identify the region of integration, which we will do by geometry and by inspecting the event. Once we have set up the integral, we have to calculate its value, which we will do by calculus or `SymPy`.

### Example 1 ###
Suppose you want to find $P(Y > 4X)$. The event is the blue region in the graph below.

```python
# NO CODE

plt.figure(figsize=(3,3))
plt.axes().set_aspect('equal')
plt.plot([0, 0], [0, 1], color='k', lw=2)
plt.plot([0, 1], [0, 1], color='k', lw=2)
plt.plot([0, 1], [1, 1], color='k', lw=2)
xx = np.arange(0, 0.251, 0.05)
yy = np.ones(len(xx))
plt.fill_between(xx, 4*xx, yy, alpha=0.3, color='blue')
plt.xlim(-0.05, 1)
plt.ylim(0, 1.05)
plt.xticks(np.arange(0, 1.1, 0.25))
plt.yticks(np.arange(0, 1.1, 0.25))
plt.xlabel('$x$')
plt.ylabel('$y$', rotation=0)
plt.title('$Y > 4X$');
```

To find the region of integration, notice that for fixed $y$, the value of $x$ goes from 0 to $y/4$. So

$$
\begin{align*}
P(Y > 4X) ~ &= ~ \int_0^1 \int_0^{y/4} 120x(y-x)(1-y)dxdy \\
&= ~ 120 \int_0^1 (1-y) \Big{(} \int_0^{y/4} (xy - x^2)dx \Big{)}dy \\
&= ~ 120\Big{(} \frac{1}{32} - \frac{1}{192} \Big{)} \int_0^1 (1-y) y^3dy \\
&= ~ 120\Big{(} \frac{1}{32} - \frac{1}{192}\Big{)} \cdot \frac{1}{20} ~ = ~ 0.15625
\end{align*}
$$

#### By `SymPy` ####

```python
Integral(f, (x, 0, y/4), (y, 0, 1))
```

```python
Integral(f, (x, 0, y/4), (y, 0, 1)).doit()
```

```python
5/32
```

### Example 2 ###
Suppose you want to find $P(X > 0.25, Y > 0.5)$. The event is the colored region below.

```python
# NO CODE

plt.figure(figsize=(3,3))
plt.axes().set_aspect('equal')
plt.plot([0, 0], [0, 1], color='k', lw=2)
plt.plot([0, 1], [0, 1], color='k', lw=2)
plt.plot([0, 1], [1, 1], color='k', lw=2)
xx = np.arange(0.25, .52, 0.05)
yy1 = 0.5*np.ones(len(xx))
yy2 = np.ones(len(xx))
plt.fill_between(xx, yy1, yy2, alpha=0.3, color='blue')
xx = np.arange(0.5, 1.1, 0.1)
yy1 = 0.5*np.ones(len(xx))
yy2 = np.ones(len(xx))
plt.fill_between(xx, xx, yy2, alpha=0.3, color='blue')
plt.xlim(-0.05, 1)
plt.ylim(0, 1.05)
plt.xticks(np.arange(0, 1.1, 0.25))
plt.yticks(np.arange(0, 1.1, 0.25))
plt.xlabel('$x$')
plt.ylabel('$y$', rotation=0)
plt.title('$X > 0.25, Y > 0.5$');
```

Now $P(X > 0.25, Y > 0.5)$ is the integral of the joint density function over this region. Notice that for each fixed value of $y > 0.5$, the value of $x$ in this event goes from $0.25$ to $y$. So let's integrate $x$ first and then $y$, as we did in the previous calculations.

$$
P(X > 0.25, Y > 0.5) ~ = ~ \int_{0.5}^1 \int_{0.25}^y 120x(y-x)(1-y)dxdy
$$

You can crank that out by hand or use `SymPy`.

```python
Integral(f, (x, 0.25, y), (y, 0.5, 1))
```

```python
Integral(f, (x, 0.25, y), (y, 0.5, 1)).doit()
```

### Expectation ###
Let $g$ be a function on the plane. Then

$$
E(g(X, Y)) ~ = ~ \int_y \int_x g(x, y)f(x, y)dxdy
$$
provided the integral exists, in which case it can be carried out in either order: $x$ first and then $y$, or the other way around.

This is an application of the non-linear function rule for expectation, applied to two random variables with a joint density.

As an example, let's find $E(\frac{Y}{X})$ for $X$ and $Y$ with the joint density $f$ used in the examples above.

Here $g(x, y) = \frac{y}{x}$, and

$$
\begin{align*}
E\big{(}\frac{Y}{X}\big{)} ~ &= ~ \int_y \int_x g(x, y)f(x, y)dxdy \\ \\
&= ~ \int_0^1 \int_0^y \frac{y}{x} 120x(y-x)(1-y)dx dy \\ \\
&= ~ 120 \int_0^1 y(1-y) \Big{(}\int_0^y (y-x)dx \Big{)}dy \\
&= ~ 60 \int_0^1 y^3(1-y) dy ~ = ~ 60 \cdot \frac{1}{20} ~ = ~ 3
\end{align*}
$$

#### By `SymPy` ####
Remember that `x` and `y` have already been defined as symbolic variables that are positive.

```python
ev_y_over_x = Integral((y/x)*f, (x, 0, y), (y, 0, 1))
ev_y_over_x
```

```python
ev_y_over_x.doit()
```

```python

```


```python
%pylab inline
pylab.rcParams['figure.figsize'] = (16.0, 8.0)
```

    Populating the interactive namespace from numpy and matplotlib

# Drawing from multivariate distributions

Draws from a multivariate normal distribution
$$ N(\mathbf{\mu}, \mathbf{\Sigma}) $$
can be generated by

1) Calculate the Cholesky decomposition $\mathbf{\Sigma} = \mathbf{R}^T\mathbf{R}$
``` python
    from numpy.linalg import cholesky
    R = cholesky(Sigma)
```

2) Generate standard normally distributed values $\mathbf{Z}$

3) Evaluate $$ \mathbf{X} = \mathbf{\mu 1} + \mathbf{RZ}$$

### Exercise 2.1

Draw 1000 samples from the bivariate distribution
$$ N\left(\left( \begin{array}{c}
0.2 \\ -1.0
\end{array}\right),
\left(\begin{array}{cc}
0.01 & -0.014 \\ -0.014 & 0.04
\end{array}\right)
\right)
$$

```python
# solution for exercise 2.1

draws = 1000

# mean
mu = array([0.2, -1.0])
# covariance matrix
Sigma = array([[0.01, -0.014],[-0.014, 0.04]])
# Cholesky factor
R = linalg.cholesky(Sigma)
# standard normal distributed values (dimension 2x1000)
Z = random.randn(2,draws)
# transformation to non-standard normal
X = dot(mu[:,newaxis], ones((1, draws))) + dot(R, Z)
# transpose to match output of built-in method
X = X.T
```

```python
figure()
scatter(X[:,0], X[:,1], 100, edgecolor="none");
```

Draws from the multivariate normal distribution can more easily generated using the built-in **scipy** functions

```python
    from scipy.stats import multivariate_normal
    X = multivariate_normal(mu, Sigma, size=1000)
```
**Note**
Scipy *rvs* functions return arrays of shape (number of draws, size of mean)

### Exercise 2.2

Repeat Exercise 2.1 with the built-in scipy function and compare the results.

```python
# solution for exercise 2.2
from scipy.stats import multivariate_normal

Xb = multivariate_normal.rvs(mu, Sigma, size=draws)

figure()
scatter(Xb[:,0],Xb[:,1],100,color="b", edgecolor="none",label="built-in function")
scatter(X[:,0],X[:,1],100, color="r", edgecolor="none",label="formula",alpha=0.2)
legend();
```

## Draws using a copula function

In many practical cases knowledge about the input quantities is available in terms of their individual distributions and a correlation coefficient. This is insufficient to assign a unique multivariate distribution. Therefore, a copula function can be defined
$$
C(\mu_1,\ldots,\mu_N) = \mathbb{P} \left[ X_1\leq G_{X_1}^{-1}(\mu_1)\ldots,X_N\leq G_{X_N}^{-1}(\mu_N) \right]
$$

### Example copula functions

* all input quantities are mutually independent
$$ C(\mu_1,\ldots,\mu_N) = \prod_{k=1}^N \mu_k $$

* the input quantities are correlated with $\rho\equiv 1$
$$ C(\mu_1,\ldots,\mu_N) = \min_{k} \mu_k $$

* two input quantities are correlated with $\rho$
$$ C(\mu_1,\mu_2) = F_2(G_{X_1}^{-1}(\mu_1),G_{X_2}^{-1}(\mu_2),\rho) $$

The copula can be used to incorporate the correlation coefficient and the individual distributions $g_{X_i}$ to formally define a multivariate distribution.

#### Example
Input quantities $X_1,X_2$ with correlation coefficient $\rho$ and
\begin{align}
X_1 \sim & N(\mu, \sigma) \\
X_2 \sim & U(a, b)
\end{align}

Use bivariate normal copula function:

1) Draw from bivariate standard normal distribution
$$ z \sim N\left(\mathbf{0}, \left(\begin{array}{cc}
1.0 & \rho \\ \rho & 1.0
\end{array}\right) \right)
$$

2) Evaluate cumulative distribution function of the copula
\begin{align}
\zeta_1 =& G_N(z_1) \\
\zeta_2 =& G_N(z_2)
\end{align}

3) Evaluate inverse cumulative distribution functions
\begin{align}
x_1 =& G_{X_1}^{-1}(\zeta_1) \\
x_2 =& G_{X_2}^{-1}(\zeta_2)
\end{align}

### Exercise 2.3

Consider the input quantities $X_1,X_2$ with
* $X_1$ has best estimate 0.2 with uncertainty of 50%
* $X_2$ has best estimate -1.0 with uncertainty of 20%
* correlation between $X_1$ and $X_2$ is $\rho=-0.7$

Generate 1000 random draws using a bivariate normal copula function.

```python
# solution for exercise 2.3
from scipy.stats import norm

rho = -0.7    # correlation coefficient
draws = 1000  # number of random draws

# step 1: draw from bivariate normal distribution
z = multivariate_normal.rvs(zeros(2), array([[1.0, rho],[rho, 1.0]]), size=draws)

# step 2: evaluate cumulative distribution function of the copula
xi1 = norm.cdf(z[:,0])
xi2 = norm.cdf(z[:,1])

# step 3: evaluate inverse cumulative distr. function of the marginal distributions
x1 = norm.ppf(xi1, loc=0.2, scale = 0.1)
x2 = norm.ppf(xi2, loc=-1.0,scale = 0.2)

figure()
scatter(Xb[:,0],Xb[:,1],100,color="b", edgecolor="none",label="built-in function")
scatter(X[:,0],X[:,1],100, color="r", edgecolor="none",label="formula",alpha=0.2)
scatter(x1, x2, 100, color="g", edgecolor="none",label="copula",alpha=0.2)
legend();
```

### Exercise 2.4

Consider the input quantities $X_1, X_2$ with

* $X_1$ has best estimate $x_1=2.4$ with expanded uncertainty $U=0.4 (k=2)$ under normality assumption

* $X_2$ is located in $[-1.5, 1.5]$

* $X_1, X_2$ are correlated with $\rho = 0.4$

Draw 1000 samples from their joint probability distribution using a normal distribution copula function.

```python
# solution for exercise 2.4
from scipy.stats import norm, uniform, multivariate_normal
rho=0.4       # correlation coefficient
draws=1000    # number of random draws

# step 1: draw from bivariate normal distribution
z = multivariate_normal.rvs(zeros(2), array([[1.0, rho],[rho, 1.0]]), size=draws)

# step 2: evaluate cumulative distribution function of the copula
xi1 = norm.cdf(z[:,0])
xi2 = norm.cdf(z[:,1])

# step 3: evaluate inverse cumulative distr. function of the marginal distributions
x1 = norm.ppf(xi1, loc=2.4, scale=0.2)
x2 = uniform.ppf(xi2, loc=-1.5, scale=3)

figure()
subplot(221)
scatter(x1, x2, 100, edgecolor="none")
ylim(-2,2); xlim(1.6,3.2)
subplot(222)
hist(x2,bins=100, orientation="horizontal",edgecolor="none")
ylim(-2,2)
subplot(223)
hist(x1,bins=100,edgecolor="none")
xlim(1.6,3.2);
```

02 Drawing from multivariate distributions.ipynb