# Best response polytopes - solutions

1. Define a polytope as a convex hull

   Bookwork: https://vknight.org/gt/chapters/06/#Definition-of-a-Polytope-as-a-convex-hull

2. Define a polytope as an intersection of halfspaces

   Bookwork: https://vknight.org/gt/chapters/06/#Definition-of-a-Polytope-as-an-intersection-of-halfspaces

3. For the following set of vertices draw the corresponding polytope **and** obtain their definition as an intersection of halfspaces:

   `A`. V = $\{(0, 0), (0, 1), (1, 0), (1, 1)\}$

```python
%matplotlib inline

import matplotlib.pyplot as plt
import numpy as np
import scipy.spatial

V = [np.array([0, 0]), np.array([0, 1]), np.array([1, 0]), np.array([1, 1])]
P = scipy.spatial.ConvexHull(V)
scipy.spatial.convex_hull_plot_2d(P);
```

The definition as an intersection of halfspaces:

$$
\begin{align}
- x_1       & \leq 0\\
-x_2        & \leq 0\\
x_1        & \leq 1\\
x_2        & \leq 1
\end{align}
$$

`B`. V = $\{(0, 0), (0, 1/4), (1, 2/3), (2, 1/5)\}$

```python
V = [np.array([0, 0]), np.array([0, 1 / 4]), np.array([1, 2  / 3]), np.array([2, 1 / 5])]
P = scipy.spatial.ConvexHull(V)
scipy.spatial.convex_hull_plot_2d(P);
```

Let us first find the equations for each boundary:

- Between $(0, 0)$ and $(0, 1/4)$: $x_1=0$
- Between $(0, 1/4)$ and $(1, 2/3)$: $x_2=(5/12)x_1+1/4$
- Between $(1, 2/3)$ and $(2, 1/5)$: $x_2=(-7/15)x_1+17/15$
- Between $(2, 1/5)$ and $(0, 0)$: $x_2=(1/10)x_1$

The definition as an intersection of halfspaces (obtained by identifying the lines corresponding to each boundary of the polytope):

$$
\begin{align}
-x_1       & \leq 0\\
12x_2 - 5x_1       & \leq 3\\
15x_2 + 7x_1        & \leq 17\\
x_1 -10x_2       & \leq 0
\end{align}
$$

`C`. V = $\{(0, 0), (0, 1/4), (1, 2/3), (2, 1/5), (1, 0)\}$

```python
V = [np.array([0, 0]), np.array([0, 1 / 4]), np.array([1, 2  / 3]), np.array([2, 1 / 5]), np.array([1, 0])]
P = scipy.spatial.ConvexHull(V)
scipy.spatial.convex_hull_plot_2d(P);
```

Let us first find the equations for each boundary:

- Between $(0, 0)$ and $(0, 1/4)$: $x_1=0$
- Between $(0, 1/4)$ and $(1, 2/3)$: $x_2=(5/12)x_1+1/4$
- Between $(1, 2/3)$ and $(2, 1/5)$: $x_2=(-7/15)x_1+17/15$
- Between $(2, 1/5)$ and $(1, 0)$: $x_2=(1/5)x_1-1/5$
- Between $(1, 0)$ and $(0, 0)$: $x_2=0$

The definition as an intersection of halfspaces (obtained by identifying the lines corresponding to each boundary of the polytope):

$$
\begin{align}
-x_1       & \leq 0\\
12x_2 - 5x_1       & \leq 3\\
15x_2 + 7x_1        & \leq 17\\
x_1 - 5 x_2      & \leq 1\\
-x_2       & \leq 0
\end{align}
$$

`4`. Define the best response polytopes.

   Bookwork: https://vknight.org/gt/chapters/06/#Definition-of-best-response-polytopes

`5`. For the following games, obtain the best response polytopes, label the vertices and identify all Nash equilibria:
   1. $
   A =
   \begin{pmatrix}
   3 & -1\\
   2 & 7\end{pmatrix}
   \qquad
   B =
   \begin{pmatrix}
   -3 & 1\\
   1 & -6\end{pmatrix}
   $

   First we need nonegative valued matrices:

   $$
   A \to A + 2 =
   \begin{pmatrix}
   5 & 1\\
   4 & 9\end{pmatrix}
   \qquad
   B \to B + 7 =
   \begin{pmatrix}
   4 & 8\\
   8 & 1\end{pmatrix}
   $$

   The inequalities of $\mathcal{P}$ are then given by:

   $$
   \begin{align}
    -x_1       & \leq 0\\
    -x_2       & \leq 0\\
    4x_1 + 8x_2       & \leq 1\\
    8x_1 + 1x_2        & \leq 1\
    \end{align}
   $$

   which corresponds to:

   $$
   \begin{align}
    x_1       & \geq 0\\
    x_2       & \geq 0\\
    x_2       & \leq 1/8 - (1/2)x_1\\
    x_2       & \leq 1 - 8x_1
    \end{align}
   $$

   The vertices are given by:

   $$V=\{(0, 0), (0, 1/8), (1/8, 0), (7/60, 1/15)\}$$

   The labels of our vertices (using the ordering of the inequalities):

   - $(0, 0)$: $\{0, 1\}$
   - $(0, 1/8)$: $\{0, 2\}$
   - $(1/8, 0)$: $\{1, 3\}$
   - $(7/60, 1/15)$: $\{2, 3\}$

   The inequalities of $\mathcal{Q}$ are given by:

   $$
   \begin{align}
    5x_1 + x_2       & \leq 1\\
    4x_1 + 9x_2        & \leq 1\\
    -x_1       & \leq 0\\
    -x_2       & \leq 0
    \end{align}
   $$

   which corresponds to:

   $$
   \begin{align}
    x_2       & \leq 1 - 5x_1\\
    x_2       & \leq 1/9 - (4/9)x_1\\
    x_1       & \geq 0\\
    x_2       & \geq 0
    \end{align}
   $$

   The vertices are given by:

   $$V=\{(0, 0), (0, 1/9), (1/5, 0), (8/41, 1/41)\}$$

   The labels of our vertices (using the ordering of the inequalities):

       - $(0, 0)$: $\{2, 3\}$
       - $(0, 1/9)$: $\{1, 2\}$
       - $(1/5, 0)$: $\{0, 3\}$
       - $(8/41, 1/41)$: $\{0, 1\}$

   The only fully labeled vertex pair is given by:

   $$((7/60, 1/15), (8/41, 1/41))$$

   which gives the normalised Nash equilibrium:

   $$((7/11, 4/11), (8/9, 1/9))$$

   Here is some code to verify this numerically:

```python
import nashpy as nash
# Obtaining the row player best response polytope vertices
B = np.array([[4, 8], [8, 1]])
halfspaces = nash.polytope.build_halfspaces(B.transpose())
for v, l in nash.polytope.non_trivial_vertices(halfspaces):
    print(v)
```

    [0.125 0.   ]
    [0.    0.125]
    [0.11666667 0.06666667]

```python
# Obtaining the column player best response polytope vertices
A = np.array([[5, 1], [4, 9]])
halfspaces = nash.polytope.build_halfspaces(A)
for v, l in nash.polytope.non_trivial_vertices(halfspaces):
    print(v)
```

    [6.93889390e-18 1.11111111e-01]
    [0.19512195 0.02439024]
    [ 2.0000000e-01 -6.9388939e-18]

```python
# Verifying the Nash equilibria:
A = A - 2  # Scaling back down (note this makes no difference)
B = B - 7
game = nash.Game(A, B)
list(game.vertex_enumeration()), (7/11, 4/11), (8/9, 1/9)
```

    ([(array([0.63636364, 0.36363636]), array([0.88888889, 0.11111111]))],
     (0.6363636363636364, 0.36363636363636365),
     (0.8888888888888888, 0.1111111111111111))

   `2`. $
   A =
   \begin{pmatrix}
   2 & -1\\
   1 & 3\end{pmatrix}
   \qquad
   B =
   \begin{pmatrix}
   -2 & 2\\
   1 & -2\end{pmatrix}
   $

   First we need nonegative valued matrices:

   $$
   A \to A + 2 =
   \begin{pmatrix}
   4 & 1\\
   3 & 5\end{pmatrix}
   \qquad
   B \to B + 3 =
   \begin{pmatrix}
   1 & 5\\
   4 & 1\end{pmatrix}
   $$

   The inequalities of $\mathcal{P}$ are then given by:

   $$
   \begin{align}
    -x_1       & \leq 0\\
    -x_2       & \leq 0\\
    x_1 + 4x_2       & \leq 1\\
    5x_1 + x_2        & \leq 1\
    \end{align}
   $$

   which corresponds to:

   $$
   \begin{align}
    x_1       & \geq 0\\
    x_2       & \geq 0\\
    x_2       & \leq 1/4 - (1/4)x_1\\
    x_2       & \leq 1 - 5x_1
    \end{align}
   $$

   The vertices are given by:

   $$V=\{(0, 0), (0, 1/4), (1/5, 0), (3/19, 4/19)\}$$

   The labels of our vertices (using the ordering of the inequalities):

   - $(0, 0)$: $\{0, 1\}$
   - $(0, 1/4)$: $\{0, 2\}$
   - $(1/5, 0)$: $\{1, 3\}$
   - $(3/19, 4/19)$: $\{2, 3\}$

   The inequalities of $\mathcal{Q}$ are given by:

   $$
   \begin{align}
    4x_1 + x_2       & \leq 1\\
    3x_1 + 5x_2        & \leq 1\\
    -x_1       & \leq 0\\
    -x_2       & \leq 0
    \end{align}
   $$

   which corresponds to:

   $$
   \begin{align}
    x_2       & \leq 1 - 4x_1\\
    x_2       & \leq 1/5 - (3/5)x_1\\
    x_1       & \geq 0\\
    x_2       & \geq 0
    \end{align}
   $$

   The vertices are given by:

   $$V=\{(0, 0), (0, 1/5), (1/4, 0), (4/17, 1/17)\}$$

   The labels of our vertices (using the ordering of the inequalities):

   - $(0, 0)$: $\{2, 3\}$
   - $(0, 1/5)$: $\{1, 2\}$
   - $(1/4, 0)$: $\{0, 3\}$
   - $(4/17, 1/17)$: $\{0, 1\}$

   The only fully labeled vertex pair is given by:

   $$((3/19, 4/19), (4/17, 1/17))$$

   which gives the normalised Nash equilibrium:

   $$((3/7, 4/7), (4/5, 1/5))$$

   Here is some code to verify this numerically:

```python
# Obtaining the row player best response polytope vertices
B = np.array([[1, 5], [4, 1]])
halfspaces = nash.polytope.build_halfspaces(B.transpose())
for v, l in nash.polytope.non_trivial_vertices(halfspaces):
    print(v)
```

    [0.2 0. ]
    [0.   0.25]
    [0.15789474 0.21052632]

```python
# Obtaining the column player best response polytope vertices
A = np.array([[4, 1], [3, 5]])
halfspaces = nash.polytope.build_halfspaces(A)
for v, l in nash.polytope.non_trivial_vertices(halfspaces):
    print(v)
```

    [0.  0.2]
    [0.23529412 0.05882353]
    [2.50000000e-01 1.38777878e-17]

```python
# Verifying the Nash equilibria:
A = A - 2  # Scaling back down (note this makes no difference)
B = B - 3
game = nash.Game(A, B)
list(game.vertex_enumeration()), (3 / 7, 4 / 7), (4 / 5, 1 / 5)
```

    ([(array([0.42857143, 0.57142857]), array([0.8, 0.2]))],
     (0.42857142857142855, 0.5714285714285714),
     (0.8, 0.2))


```python
# Importing packages
import matplotlib.pyplot as plt
import pandas as pd
import ipywidgets as widgets
import numpy as np
import itertools
from ipywidgets import Layout
import seaborn as sns
import math

import time
from scipy import linalg
import scipy.optimize as optimize
import sympy as sm

from tkinter import *
import tkinter as tk

```

In this project, we want to try to estimate the ratings of movies. That is, we well calculate the utility of a movie, given genres, duration, awards and so forth, and we will try to calculate the IMDB ratings for each movie, based on this utility. Finally, we will then minimize the distance between our estimated ratings and the actual ratings.

Our utility function is given by:
$$ U_i = \sum_{k=1}^{23}(\alpha_k G_{ik}) + \sum_{k=1920}^{2010} (\beta_k D_{ik}) + \gamma N_i + \delta W_i + \rho_1 L_i + \rho_2 L_i^2 $$.

$$\text{Where, } G_{ik} \text{ is genre } k \text{, } D_{ik} \text{ is decade } k \text{, } N_i \text{ is nominations, } W_i \text{ is number of wins and } L_i \text{ is duration. } $$

Based on this utility function, we will estimate the ratings of each movies,
$$ R_i^{model} = \frac{\exp(\omega x_i)}{1 + \exp(\omega x_i)} $$

where,
$$ x_i = \big[G_1, G_2, ..., G_{23}, D_{1920}, D_{1930}, ..., D_{2010}, N_i, W_i, L_i, L_i^2 \big] $$
$$ \omega = \big[\alpha_1, \alpha_2, ..., \alpha_{23}, \beta_{1920}, \beta_{1930}, \beta_{2010}, \gamma, \delta, \rho_1, \rho_2  \big] $$

We then use optimize methods to solve the following:

$$ \min_{\omega} \Big\{ \sum_{i=1}^{n} \left( R_i^{model} - R_i^{data} \right)^2 \Big\} $$

```python
import numpy as np
import pandas as pd
import itertools
import time
import scipy.optimize as optimize
from data_gen import gen_df

filename = 'imdb.csv'

df = gen_df(filename)

decade_list = [1920, 1930, 1940, 1950, 1960, 1970, 1980, 1990, 2000, 2010]

# Decade dummies
for i in decade_list:
    df[f'decade_{i}'] = 0
    df.loc[(df['decade'] == f'{i}s'),f'decade_{i}'] = 1

#print(imdb.shape)

df = df.loc[(df['ratingCount']>=5000)]
df.drop(columns=['Adult','GameShow','News','RealityTV','TalkShow'], inplace=True)

# df['duration_sqr'] = df['duration']*df['duration']

df_X = df.copy()
df_Y = pd.DataFrame(df['imdbRating'].copy())
df_Y.rename(columns = {'imdbRating':'rat_data'}, inplace=True)

#print(type(df_Y))
#print(df_Y.head())
#print(df_X.head())

drops = ['index', 'tid', 'title', 'imdbRating', 'ratingCount', 'year',
         'nrOfGenre', 'nrOfPhotos', 'nrOfNewsArticles',  'nrOfUserReviews', 'decade']

for i in drops:
    df_X.drop(columns=[i], inplace=True)

#print(df_X.head())

# Rearrange columns
df_X = df_X.reindex(['Action','Adventure','Animation','Biography','Comedy','Crime','Documentary',
                          'Drama','Family','Fantasy','FilmNoir','History','Horror','Music','Musical',
                          'Mystery','Romance','SciFi','Short','Sport','Thriller','War','Western',
                          'decade_1920','decade_1930','decade_1940','decade_1950','decade_1960',
                          'decade_1970','decade_1980','decade_1990','decade_2000','decade_2010',
                          'nrOfNominations','nrOfWins','duration'], axis=1)

```

```python
#print(df_X.head())
omegas = np.random.uniform(0,1,36)

df_Y['util'] = df_X@omegas
df_Y['rat_model'] = 10*np.exp(df_Y['util'])/(1+np.exp(df_Y['util']))
df_Y['sqr_diff'] = (df_Y['rat_model']-df_Y['rat_data'])**2

def sqr_diff_sum(df_X,pars):
    util = df_X@pars
    df_Y['rat_model'] = 10*np.exp(util)/(1+np.exp(util))
    df_Y['sqr_diff'] = (df_Y['rat_model']-df_Y['rat_data'])**2
    return df_Y['sqr_diff'].sum()

#print(df_X.head())
#print(df_Y.head())

def collect(x):
    global fs
    global evals
    global x0

    if evals == 0:
        fs = test[x0]

    fs.append(test(x))

    evals += 1

# Scipy minimize
evals = 0
def zeros(n):
    list = [0] * n
    return list

test = lambda x: sqr_diff_sum(df_X,x)

x0 = zeros(36)
result = optimize.minimize(test,x0,
                           method="Nelder-Mead",
                           options={"disp":True, "maxiter": 1000000} # display the results
                           )
```

    Optimization terminated successfully.
             Current function value: 3735.382286
             Iterations: 16566
             Function evaluations: 19203

```python
result.x

liste = ['Action','Adventure','Animation','Biography','Comedy','Crime','Documentary',
                          'Drama','Family','Fantasy','FilmNoir','History','Horror','Music','Musical',
                          'Mystery','Romance','SciFi','Short','Sport','Thriller','War','Western',
                          'decade_1920','decade_1930','decade_1940','decade_1950','decade_1960',
                          'decade_1970','decade_1980','decade_1990','decade_2000','decade_2010',
                          'nrOfNominations','nrOfWins','duration']

ditce = dict()

for j,i in enumerate(liste):
    ditce[i] = f'{result.x[j]:.2f}'
```

```python
df_X = df_X.reindex(['Action','Adventure','Animation','Biography','Comedy','Crime','Documentary',
                          'Drama','Family','Fantasy','FilmNoir','History','Horror','Music','Musical',
                          'Mystery','Romance','SciFi','Short','Sport','Thriller','War','Western',
                          'decade_1920','decade_1930','decade_1940','decade_1950','decade_1960',
                          'decade_1970','decade_1980','decade_1990','decade_2000','decade_2010',
                          'nrOfNominations','nrOfWins','duration'], axis=1)

# Figure ideas

"""
Two figures:
    Left figure:
        Two curves, one which is actual average rating, while the other is estimated rating.
        Each observation on the estimated ratings curve is an estimate from our score function.
        By each iteration, we wish to observe a decreased gap between the two functions.
    Right figure:
        Histogram of the squared errors. Each bin should be a new iteration, and preferably, we would
        see a decreasing bin height, as the squared errors converges towards a minimum.
"""

```

    ' \nTwo figures: \n    Left figure: \n        Two curves, one which is actual average rating, while the other is estimated rating. \n        Each observation on the estimated ratings curve is an estimate from our score function. \n        By each iteration, we wish to observe a decreased gap between the two functions. \n    Right figure: \n        Histogram of the squared errors. Each bin should be a new iteration, and preferably, we would \n        see a decreasing bin height, as the squared errors converges towards a minimum. \n'

```python
# From Seb_workspace

def df_s(keep_top=None):
    global df
    global decade_list

    filename = 'imdb.csv'

    df = gen_df(filename)

    decade_list = [1920, 1930, 1940, 1950, 1960, 1970, 1980, 1990, 2000, 2010]

    # Decade dummies
    for i in decade_list:
        df[f'decade_{i}'] = 0
        df.loc[(df['decade'] == f'{i}s'),f'decade_{i}'] = 1

    df = df.loc[(df['ratingCount']>=5000)]
    df.drop(columns=['Adult','GameShow','News','RealityTV','TalkShow'], inplace=True)

    if keep_top != None:
        df = df.sort_values('imdbRating', ascending=False)
        df = df.iloc[:keep_top]

    df_X = df.copy()
    df_Y = pd.DataFrame(df['imdbRating'].copy())
    df_Y.rename(columns = {'imdbRating':'rat_data'}, inplace=True)

    drops = ['index', 'tid', 'title', 'imdbRating', 'ratingCount', 'year',
             'nrOfGenre', 'nrOfPhotos', 'nrOfNewsArticles',  'nrOfUserReviews', 'decade']

    for i in drops:
        df_X.drop(columns=[i], inplace=True)

    # Rearrange columns
    df_X = df_X.reindex(['Action','Adventure','Animation','Biography','Comedy','Crime','Documentary',
                              'Drama','Family','Fantasy','FilmNoir','History','Horror','Music','Musical',
                              'Mystery','Romance','SciFi','Short','Sport','Thriller','War','Western',
                              'decade_1920','decade_1930','decade_1940','decade_1950','decade_1960',
                              'decade_1970','decade_1980','decade_1990','decade_2000','decade_2010',
                              'nrOfNominations','nrOfWins','duration'], axis=1)

    return df_X, df_Y

def optimizer(keep_top=None):
    global fs
    global evals
    global x0
    global df

    def sqr_diff_sum(df_X,pars):
        util = df_X@pars
        df_Y['rat_model'] = 10*np.exp(util)/(1+np.exp(util))
        df_Y['sqr_diff'] = (df_Y['rat_model']-df_Y['rat_data'])**2
        return df_Y['sqr_diff'].sum()

    # Scipy minimize
    def zeros(n):
        list = [0] * n
        return list

    def collect(x):
        global fs
        global evals
        global x0
        global fig
        global ax
        global x_1
        global x_2
        global x_3
        global x_4
        global x_5
        global x_6
        global x_7
        global x_8
        global x_9
        global x_10
        global x_11
        global x_12
        global x_13
        global x_14
        global x_15
        global x_16
        global x_17
        global x_18
        global x_19
        global x_20
        global x_21
        global x_22
        global x_23
        global x_24
        global x_25
        global x_26
        global x_27
        global x_28
        global x_29
        global x_30
        global x_31
        global x_32
        global x_33
        global x_34
        global x_35
        global x_36
        # for i in range(1,37,1):
            # global x_i

        if evals == 0:
            fs = []
            fs.append(obj_fun(x0))
            x_1 = [x0[0]]
            x_2 = [x0[1]]
            x_3 = [x0[2]]
            x_4 = [x0[3]]
            x_5 = [x0[4]]
            x_6 = [x0[5]]
            x_7 = [x0[6]]
            x_8 = [x0[7]]
            x_9 = [x0[8]]
            x_10 = [x0[9]]
            x_11 = [x0[10]]
            x_12 = [x0[11]]
            x_13 = [x0[12]]
            x_14 = [x0[13]]
            x_15 = [x0[14]]
            x_16 = [x0[15]]
            x_17 = [x0[16]]
            x_18 = [x0[17]]
            x_19 = [x0[18]]
            x_20 = [x0[19]]
            x_21 = [x0[20]]
            x_22 = [x0[21]]
            x_23 = [x0[22]]
            x_24 = [x0[23]]
            x_25 = [x0[24]]
            x_26 = [x0[25]]
            x_27 = [x0[26]]
            x_28 = [x0[27]]
            x_29 = [x0[28]]
            x_30 = [x0[29]]
            x_31 = [x0[30]]
            x_32 = [x0[31]]
            x_33 = [x0[32]]
            x_34 = [x0[33]]
            x_35 = [x0[34]]
            x_36 = [x0[35]]
        elif evals < 5 and evals != 0:
            print(f'Iteration {evals:6.0f}: Function value = {obj_fun(x):9.2f}')
            time.sleep(.5)
        elif evals == 5:
            print(f'Iteration {evals:6.0f}: Function value = {obj_fun(x):9.2f}')
            print('...')
        elif evals%1000 == 0:
            print(f'Iteration {evals:6.0f}: Function value = {obj_fun(x):9.2f}')

        fs.append(obj_fun(x))
        x_1.append(x[0])
        x_2.append(x[1])
        x_3.append(x[2])
        x_4.append(x[3])
        x_5.append(x[4])
        x_6.append(x[5])
        x_7.append(x[6])
        x_8.append(x[7])
        x_9.append(x[8])
        x_10.append(x[9])
        x_11.append(x[10])
        x_12.append(x[11])
        x_13.append(x[12])
        x_14.append(x[13])
        x_15.append(x[14])
        x_16.append(x[15])
        x_17.append(x[16])
        x_18.append(x[17])
        x_19.append(x[18])
        x_20.append(x[19])
        x_21.append(x[20])
        x_22.append(x[21])
        x_23.append(x[22])
        x_24.append(x[23])
        x_25.append(x[24])
        x_26.append(x[25])
        x_27.append(x[26])
        x_28.append(x[27])
        x_29.append(x[28])
        x_30.append(x[29])
        x_31.append(x[30])
        x_32.append(x[31])
        x_33.append(x[32])
        x_34.append(x[33])
        x_35.append(x[34])
        x_36.append(x[35])
        evals += 1

    df_X, df_Y = df_s(keep_top)
    x0 = zeros(36)
    evals = 0

    obj_fun = lambda x: sqr_diff_sum(df_X,x)

    result = optimize.minimize(obj_fun,x0,
                               method="Nelder-Mead",
                               options={"disp":True, "maxiter":50000}, # display the results
                               callback=collect
                               )

    # Progress figure
    fig = plt.figure()
    ax = fig.add_subplot(111)
    ax.set_title('Progress')
    ax.set_xlabel('Iterations')
    ax.set_ylabel('Function value')
    ax.set_xlim(0,20000)
    ax.set_ylim(0,20000)
    ax.plot(range(evals+1),fs)
    plt.show()

    return result

result = optimizer(keep_top=2000)

result.x

variables = ['Action','Adventure','Animation','Biography','Comedy','Crime','Documentary',
                          'Drama','Family','Fantasy','FilmNoir','History','Horror','Music','Musical',
                          'Mystery','Romance','SciFi','Short','Sport','Thriller','War','Western',
                          'decade_1920','decade_1930','decade_1940','decade_1950','decade_1960',
                          'decade_1970','decade_1980','decade_1990','decade_2000','decade_2010',
                          'nrOfNominations','nrOfWins','duration']

results = dict()

for j,i in enumerate(variables):
    results[i] = result.x[j]

```

```python
results = dict()

for j,i in enumerate(variables):
    results[i] = result.x[j]

# Dictionary with parameters
preben = dict()
for j,i in enumerate(liste):
    preben[i] = f'{result.x[j]:.2f}'

print(preben)
```

    {'Action': '-0.33', 'Adventure': '-0.21', 'Animation': '0.40', 'Biography': '-0.09', 'Comedy': '-0.04', 'Crime': '0.02', 'Documentary': '0.16', 'Drama': '0.13', 'Family': '-0.07', 'Fantasy': '0.41', 'FilmNoir': '-0.11', 'History': '-0.37', 'Horror': '0.30', 'Music': '-0.22', 'Musical': '0.13', 'Mystery': '-0.09', 'Romance': '0.06', 'SciFi': '-0.07', 'Short': '-0.18', 'Sport': '-0.06', 'Thriller': '0.07', 'War': '-0.19', 'Western': '-0.51', 'decade_1920': '-0.03', 'decade_1930': '-0.12', 'decade_1940': '0.04', 'decade_1950': '0.10', 'decade_1960': '0.14', 'decade_1970': '-0.01', 'decade_1980': '0.40', 'decade_1990': '0.09', 'decade_2000': '0.10', 'decade_2010': '0.13', 'nrOfNominations': '-0.00', 'nrOfWins': '0.00', 'duration': '0.58'}

```python
parameters = [x_1, x_2, x_3, x_4, x_5, x_6, x_7, x_8, x_9, x_10,
              x_11, x_12, x_13, x_14, x_15, x_16, x_17, x_18, x_19, x_20,
              x_21, x_22, x_23, x_24, x_25, x_26, x_27, x_28, x_29, x_30,
              x_31, x_32, x_33, x_34, x_35, x_36]

par_dict = {"Action": x_1,
             "Adventure": x_2,
             "Animation": x_3,
             "Biography": x_4,
             "Comedy": x_5,
             "Crime": x_6,
             "Documentary": x_7,
             "Drama": x_8,
             "Family": x_9,
             "Fantasy": x_10,
             "Film Noir": x_11,
             "History": x_12,
             "Horror": x_13,
             "Music": x_14,
             "Musical": x_15,
             "Mystery": x_16,
             "Romance": x_17,
             "SciFi": x_18,
             "Short": x_19,
             "Sport": x_20,
             "Thriller": x_21,
             "War": x_22,
             "Western": x_23,
             "1920's": x_24,
             "1930's": x_25,
             "1940's": x_26,
             "1950's": x_27,
             "1960's": x_28,
             "1970's": x_29,
             "1980's": x_30,
             "1990's": x_31,
             "2000's": x_32,
             "2010's": x_33,
             "Nr. of Nominations": x_34,
             "Nr. of Wins": x_35,
             "Duration": x_36
            }

df_par = pd.DataFrame(par_dict)
```

```python
# Interactive graph of parametervalues for each iteration in optimizer
## Interactive: Chose which parameter to show

parameters = [x_1, x_2, x_3, x_4, x_5, x_6, x_7, x_8, x_9, x_10,
              x_11, x_12, x_13, x_14, x_15, x_16, x_17, x_18, x_19, x_20,
              x_21, x_22, x_23, x_24, x_25, x_26, x_27, x_28, x_29, x_30,
              x_31, x_32, x_33, x_34, x_35, x_36]

# Interactive graph of parameter estimates
def graph(par):
    plt.plot(df_par[par])
    plt.grid(True)
    plt.axhline(df_par[par].iloc[-1], linestyle="dashed", color="orange", label="Optimum")

    # Labels
    plt.title("Progress in Parameter Estimation")
    plt.ylabel("Parameter Estimate")
    plt.xlabel("Nr. of Iterations")
    plt.legend()

def graph_int():
    widgets.interact(graph,
                     par = widgets.Dropdown(
                     description="Variable",
                     options=df_par.columns.values,
                     value="Action"));
graph_int()

```

    interactive(children=(Dropdown(description='Parameter', options=('Action', 'Adventure', 'Animation', 'Biograph…

```python

```

```python

```

modelproject/Temp/Economicst Do It With Models.ipynb


```python
import numpy as np
from IPython.display import display, Math, Latex
from print_latex import printLatex

import pygamma as pg
```

## 2.4 Description

The GAMMA module ***SpinOpSng*** provides all the common single spin angular momentum operators. These are used internally to provide the class ***spin_op*** with the core elements needed for construction of spin operators defined over systems containing multiple spins. Normally, GAMMA users do not employ these single spin operator functions explicitly in programs. Rather, spin operators defined over the Hilbert space of a spin system are used. These are more general and will handle any number of spins in the system and in any combination. When the system contains only one spin then such spin operators will be equivalent to the single spin operators returned by the functions of this section.

Each spin operator can be presented is in terms of single spin operators. In turn, each single spin operator in this context is stored in the spin Hilbert space of dimension of *2I+1*, not in the full spin system Hilbert space. The full Hilbert space representation is obtained from these single spin matrices by taking direct products.

\begin{equation}
SOp = SOp(1) \; \otimes \; SOp(2) \; \otimes SOp(2) \; \otimes \dotsc \otimes \; SOp(n) \;\;\;\;\;\;\;\;  (2.1)
\end{equation}

In this equation, ***SOp(i)*** is the single spin operator acting on spin i. All spins which are active in ***SOp*** are included in the product and each single spin operator here spans its own Hilbert Space. Although not generally possible for any arbitrary spin operators 1 , use of this operator description can result in both computational and memory savings. The memory savings can be made evident by consideration of the spin operator $I_{1x} in a three spin system with spins having quantum numbers of 1/2, 1, and 1/2 respectively. The full Hilbert space of this spin system is 12 resulting from the product of the individual spin sub-spaces of 2, 3, and 2. The operator then looks like

\begin{equation}
I_{1x} =
\begin{bmatrix}
 12 \times 12 \\
 Array        \\
\end{bmatrix} =
\begin{bmatrix}
 0 & 1  \\
 1 & 0  \\
\end{bmatrix} \otimes
\begin{bmatrix}
 1 & 0 & 0 \\
 0 & 1 & 0 \\
 0 & 0 & 0 \\
\end{bmatrix} \otimes
\begin{bmatrix}
 1 & 0  \\
 0 & 1 \\
\end{bmatrix}  \;\;\;\;\;\;\;\;  (2.2)
\end{equation}

or equivalently

\begin{equation}
I_{1x} = I_{1x} \; \otimes \; I_{2e} \; \otimes \; I_{3e}
\;\;\;\;\;\;\;\;  (2.3)
\end{equation}

For this simple three spin system, the full Hilbert space representation is a 12 x 12 array whereas by the individual spin matrices it is significantly smaller. GAMMA wisely stores only the diagonal elements in a diagonal array, thus internally storing only 9 elements rather than 144 for this array. Computations occur along a similar vein, when GAMMA can work with this sub-space representations it will do so preferentially, hence running significantly faster than when working with the expanded form.

----

1. The question immediately arises as to when a spin operator (or any operator) can be expressed in terms of direct products. This will be true when the operator can be broken up into products of mutually commuting operators, such as the set {$I_{ix}$} for a spin system which all must commute as they act on different spins.

```python

```


 Tutorial 4

 Gaussian Processes

## Overview

 Gaussian Processes

This tutorial is based on work done by Ollie Pollard on using Gaussian Processes to predict sea level rise. Following the steps outlined in this [visual article](https://distill.pub/2019/visual-exploration-gaussian-processes/). Gaussian processes are often used to make predictions about our data by incorporating prior knowledge often to fit a function to a data set. For a given set of training points, there are potentially infinitely many functions that fit the data. Gaussian processes offer an elegant solution to this problem by assigning a probability to each of these functions. The mean of this probability distribution then represents the most probable characterization of the data.

## Recommended reading

* [Overview of Linear Regression](https://towardsdatascience.com/linear-regression-detailed-view-ea73175f6e86)
* [An Intuative Guide to Gaussian Processes](https://towardsdatascience.com/an-intuitive-guide-to-gaussian-processes-ec2f0b45c71d)

 Machine Learning Theory

This tutorial is mainly focusing on using python to explore Gaussian Processes. Please read the full [visual aritcle](https://distill.pub/2019/visual-exploration-gaussian-processes/) for a more in depth explanation.

The [gaussian distribution](https://en.wikipedia.org/wiki/Normal_distribution) forms the building blocks of Gaussian Processes.
   $\displaystyle f(x) ={\frac {1}{\sigma {\sqrt {2\pi }}}}e^{-{\frac {1}{2}}\left({\frac {x-\mu }{\sigma }}\right)^{2}}$

For Machine Learning Gaussian Processes we are interested in the [multivariate case](https://en.wikipedia.org/wiki/Multivariate_normal_distribution). The multivariate Gaussian distribution is defined by a mean vector $\mu$ (the expected value of the distribution) and a covariance matrix $\Sigma$. $\Sigma$ models the variance along each dimension and determines how the different random variables are correlated. If $X$ follows a normal distribution:

\begin{equation} X = \begin{bmatrix} X_1 \\ X_2 \\ \vdots \\ X_n \end{bmatrix} \sim \mathcal{N}(\mu, \Sigma) \end{equation}

The covariance matrix $\Sigma$ describes the shape of the distribution. It is defined in terms of the expected value $E$

\begin{equation}
\Sigma = \text{Cov}(X_i, X_j) = E \left[ (X_i - \mu_i)(X_j - \mu_j)^T \right]
\end{equation}

Gaussian distributions have the nice algebraic property of being closed under conditioning and marginalization. Being closed under conditioning and marginalization means that the resulting distributions from these operations are also Gaussian, which makes many problems in statistics and machine learning tractable.
Marginalization and conditioning both work on subsets of the original distribution and we will use the following notation:

\begin{equation}
P_{X,Y} = \begin{bmatrix} X \\ Y \end{bmatrix} \sim \mathcal{N}(\mu, \Sigma) = \mathcal{N} \left( \begin{bmatrix} \mu_X \\ \mu_Y \end{bmatrix}, \begin{bmatrix} \Sigma_{XX} \, \Sigma_{XY} \\ \Sigma_{YX} \, \Sigma_{YY} \end{bmatrix} \right)
\end{equation}

With $X$ and $Y$ representing subsets of original random variables.
Through marginalization we can extract partial information from multivariate probability distributions. In particular, given a normal probability distribution $P(X,Y)$ over vectors of random variables $X$ and $Y$, we can determine their marginalized probability distributions in the following way:

\begin{equation}
\begin{aligned}
X \sim \mathcal{N}(\mu_X, \Sigma_{XX}) \\
Y \sim \mathcal{N}(\mu_Y, \Sigma_{YY})
\end{aligned}
\end{equation}

The interpretation of this equation is that each partition $X$ and $Y$ only depends on its corresponding entries in $\mu$ and $\Sigma$. To marginalize out a random variable from a Gaussian distribution we can simply drop the variables from $\mu$ and $\Sigma$.

\begin{equation}
p_X(x) = \int_y p_{X,Y}(x,y)dy = \int_y p_{X|Y}(x|y) p_Y(y) dy
\end{equation}

The way to interpret this equation is that if we are interested in the probability density of $X=x$, we need to consider all possible outcomes of $Y$ that can jointly lead to the result.

Another important operation for Gaussian processes is conditioning. It is used to determine the probability of one variable depending on another variable. Similar to marginalization, this operation is also closed and yields a modified Gaussian distribution. This operation is the cornerstone of Gaussian processes since it allows Bayesian inference. Conditioning is defined by:

\begin{equation}
\begin{aligned}
X|Y \sim \mathcal{N}(\:\mu_X + \Sigma_{XY}\Sigma_{YY}^{-1}(Y - \mu_Y),\: \Sigma_{XX}-\Sigma_{XY}\Sigma_{YY}^{-1}\Sigma_{YX}\:) \\
Y|X \sim \mathcal{N}(\:\mu_Y + \Sigma_{YX}\Sigma_{XX}^{-1}(X - \mu_X),\: \Sigma_{YY}-\Sigma_{YX}\Sigma_{XX}^{-1}\Sigma_{XY}\:) \\
\end{aligned}
\end{equation}

The new mean only depends on the conditioned variable, while the covariance matrix is independent from this variable.

 Python

## Tensorflow and GPflow

There are many machine learning python libraries available, [TensorFlow](https://www.tensorflow.org/) a is one such library. Throughout this tutorial, you will see some complex machine learning tasks executed in just a few lines of code by calling [GPflow](https://gpflow.readthedocs.io/en/master/) functions which use Tensor flow. If you have GPUs on the machine you are using, these python libraries will automatically use them and run the code even faster!

## Further Reading

* [GPflow example Notebooks](https://gpflow.readthedocs.io/en/develop/notebooks_file.html)

 Requirements

These notebooks should run on a standard laptop with the correct python environment.

 Python Packages:

* Python 3.8
* tensorflow > 2.1
* gpflow 2.1 *(must be installed via pip to get latest version)*
* numpy
* matplotlib
* plotly
* scipy
* pandas

 Data Requirements

This notebook referes to some data included in the git hub repositroy in the [data](data) folder

**Contents:**

1. [Overview of Gausian Processes](#Overview-of-Gausian-Processes)
2. [Sea Level Example](#Sea-Level-Example)
1. [Load Data](#Load-Data)
2. [Normalise Data](#Normalise-Data)
3. [Plot Data](#Plot-Data)
4. [Define GP flow model](#Define-GP-flow-model)
5. [Optamization](#Optamization)
6. [Prediction](#Prediction)
7. [Cross Validation](#Cross-Validation)
8. [Plot High Stands](#Plot-High-Stands)

Load in all required modules and turn off warnings. If you have no [GPU's](https://www.analyticsvidhya.com/blog/2020/09/why-gpus-are-more-suited-for-deep-learning/) available you may see some tensor flow warnings

```python
# For readability: disable warnings
import warnings
warnings.filterwarnings('ignore')
```

```python
# import modules
import gpflow
import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
import plotly.express as px
from gpflow.utilities import print_summary
from scipy.spatial.distance import cdist
from scipy.stats import multivariate_normal

gpflow.config.set_default_summary_fmt("notebook")
plt.style.use("seaborn-whitegrid")
```

# Overview of Gausian Processes

Say we want to find an unknown function `y` where $y = x^3 - 9x + e^{(x^3/30)}$ from a random sample of points [`x_samp`, `ysamp`]

**NB function `y` is chosen at random for demonstative purposes you can change `y` to what ever you like and see similar results!**

After you've ran throught the code you might want to re run with a different choice of `y`

```python
# some function y that we're going to try and find
# set x to between -6 and 6
x = np.linspace(-6,6,100)
# A random function chosem
y = x**3 - 9*x + np.exp(x**3/30)
```

# generate sample

```python
# a random sample of points
# whole x range is between -6 and 6 and we'll sample x between -2 and 2
sample_size = 5

samp_index = np.random.randint((len(x)-1)/4, high=(len(x)-1)*3/4, size=sample_size)
```

Later if you wish you can uncomment the below cell to see the effects of changing the sample size or sample area (below is set to sample whole range with a sample size of 8)

```python
#sample_size = 8
#samp_index = np.random.randint(0, high=len(x), size=sample_size)
```

As we've take a random sample there's a small chance we might have sampled the same point twice so the below code is going to check we have 5 unique sample points

```python
# Check no duplicates in sample index

if len(np.unique(samp_index)) != sample_size :
    print("duplicate sample index found please rerun above cell")
```

```python
samp_index
```

```python
x_samp = x[samp_index]
y_samp = y[samp_index]
# Plot Sample points
fig, ax = plt.subplots(figsize=(10,5))
point_c = "#f77f00"
ax.scatter(x_samp, y_samp, s=50, c=point_c, zorder=10)
ax.set_xlim([-6,6])
ax.set_xlabel("x")
ax.set_ylabel("f(x)")
```

From these few points it's not obvious what function `y` could possibly be. So we can use Gaussian Processes to help work out the unknown function from these few points

The covariance matrix $\Sigma$ is determined by its covariance function $k$, which is often also called the kernel of the Gaussian process. Here we will use the [Radial Basis Function Kernal](https://towardsdatascience.com/radial-basis-function-rbf-kernel-the-go-to-kernel-acf0d22c798a):

\begin{equation}
 K(X_1,X_2) = exp(-{\frac{||X_1 - X_2|| ^2}{2\sigma^2}})
\end{equation}

where $\sigma$ is the variance and hyperparameter and $||X_1 - X_2|| ^2$ is the is the Euclidean distance between two points

this is defined in the function `rbf_kernel`

```python
# radial basis function kernel
def rbf_kernel(x1, x2, var, lscale):
    """
        Compute the Euclidean distance between each row of X and X2, or between
        each pair of rows of X if X2 is None and feed it to the kernel.
     """
    if x2 is None:
        d = cdist(x1, x1)
    else:
        d = cdist(x1, x2)
    K = var*np.exp(-np.power(d,2)/(lscale**2))
    return K
```

so `K` can be obtained via `rbf_kernel` and $\mu$ `mu` is often assumed to be zero as a starting point

try adjusting `lscale` to see what happens with the results
e.g. after running through the first time set `lscale=5` and run through all cells again

Increasing the length parameter increases the banding, as points further away from each other become more correlated.

```python
lscale = 1.0
K_rbf = rbf_kernel(x.reshape(-1,1), None, 1.0, lscale)
mu = np.zeros(x.shape[0])
f_rbf = np.random.multivariate_normal(mu, K_rbf, 100)
```

```python
rv = multivariate_normal(np.zeros(2), rbf_kernel(np.arange(0,2).reshape(-1,1), None, 1.0, 2.0))
```

```python
#Create grid and multivariate normal
a = np.linspace(-3,3,500)
b = np.linspace(-3,3,500)
X, Y = np.meshgrid(a,b)
pos = np.empty(X.shape + (2,))
pos[:, :, 0] = X
pos[:, :, 1] = Y
```

Stochastic processes, such as Gaussian processes, are essentially a set of random variables. In addition, each of these random variables has a corresponding index $i$. We will use this index to refer to the $i$-th dimension of our nnn-dimensional multivariate distributions.

Below, we have a two-dimensional normal distribution. Each dimension $y_i$ is assigned an index $\displaystyle{i \in {1,2}}$.  This representation allows us to understand the connection between the covariance and the resulting values: the underlying Gaussian distribution has a positive covariance between $y_1$ and $y_1$ -  this means that $y_2$ will increases as $y_1$ gets larger and vice versa.

```python
grid = plt.GridSpec(1, 2, wspace=0.2, hspace=0.3)
fig = plt.figure(figsize=(10, 5))
ax1 = fig.add_subplot(grid[0, 0])
ax2 = fig.add_subplot(grid[0, 1])
ax1.contourf(X, Y, rv.pdf(pos), cmap="Reds", levels=300)
for index in range(10):
    rand_y1, rand_y2 = np.random.multivariate_normal(np.zeros(2), rbf_kernel(np.arange(0,2).reshape(-1,1), None, 1.0, 2.0), 1)[0]
    ax1.scatter(rand_y1, rand_y2)
    ax1.set_xlabel("$y_1$")
    ax1.set_ylabel("$y_2$")
    ax2.plot(np.arange(1,3), [rand_y1, rand_y2], '-o')
    ax2.set_xlim([0,3])
    ax2.set_ylim([-3,3])
    ax2.set_xlabel("index")
    ax2.set_ylabel("$y$")
    ax2.set_xticks([1,2])
```

The more horizontal the lines are the more strongly correlated

```python
# rbf_kernel(X1,X2,var,lscale)
rbf_kernel(np.arange(0,2).reshape(-1,1), None, 1.0, lscale)
```

## Prior distribution

The following figure shows samples of potential functions from prior distributions (the case where we have not yet observed any training data) that were created using RBF kernel

```python
var_index = np.linspace(0, 20,100)
fig, ax = plt.subplots(figsize=(10,5))
for index in range(10):
    ax.plot(var_index, np.random.multivariate_normal(np.zeros(var_index.shape[0]), rbf_kernel(var_index.reshape(-1,1), None, 1.0, 5.0), 1)[0])
    ax.set_xlim(0,20)
    ax.set_ylim(-3,3)
    ax.set_xlabel("$x$")
    ax.set_ylabel("$y$")
    ax.set_xticks([])
ax.set_title('GP samples from RBF where lscale ='+str(lscale))
```

```python
# Covariance matrix from the RBF kernel
# rbf_kernel(X1,X2,var,lscale)
K_rbf = rbf_kernel(x.reshape(-1,1), None, 1.0, lscale)
# mu = 0
mu = np.zeros(x.shape[0])
# fuctions from rbf kernel
f_rbf = np.random.multivariate_normal(mu, K_rbf, 100)
```

```python
# plot 100 GP samples
index=100
fig, ax = plt.subplots(figsize=(10,5))
ax.plot(x, f_rbf.T[:,0:index], color='C0', alpha=0.2)
ax.set_xlim([-6,6])
# depending you you function choice you might need to comment out the ylim line below
ax.set_ylim([-10,15])
ax.set_xlabel("x")
ax.set_ylabel("f(x)")
ax.set_title('100 GP samples from  RBF kernel where lscale ='+str(lscale))
```

as $\mu$ is set to zero all functions are distributed normally around the mean $\mu$ (0)

## Posterior distribution

now we're going to activate training data which we can add back into our distribution. To give the posterior distribution (where we have incorporated the training data into our model).

1. First, we form the joint distribution between all x points `x` and the training points `x_samp` which gives `k_starX`
2. we can the use `k_xx` (covariance matrix of test x points) `k_starstar` covariance matrix of all x to calculate $\mu_{pred}$ and $K_{pred}$

\begin{equation}
\boldsymbol \mu_{\text{pred}} = \mathbf{K}_*^\top \left[\mathbf{K} + \sigma^2 \mathbf{I}\right]^{-1} \mathbf{y}
\end{equation}

\begin{equation}
\mathbf{K}_{\text{pred}} = \mathbf{K}_{*,*} - \mathbf{K}_*^\top \left[\mathbf{K} + \sigma^2 \mathbf{I}\right]^{-1} \mathbf{K}_*.
\end{equation}

```python
k_starX = rbf_kernel(x.reshape(-1,1), x_samp.reshape(-1,1), 3, lscale)

# K from sample point
k_xx = rbf_kernel(x_samp.reshape(-1,1), None, 3, lscale)
k_starstar = rbf_kernel(x.reshape(-1,1), None, 3, lscale)
```

```python
print('no training data multivariate Gaussian distribution shape = ' +str(k_starstar.shape))
print('sample-whole multivariate Gaussian distribution shape = ' +str(k_starX.shape))
print('whole multivariate Gaussian distribution shape = ' +str(k_starX.shape))
```

```python
mu = k_starX.dot(np.linalg.inv(k_xx)).dot(y_samp).flatten()
var = k_starstar - k_starX.dot(np.linalg.inv(k_xx)).dot(k_starX.T)
```

If you get an error here please return to [generate sample](#generate-sample) section to check for duplicated sample points

In the constrained covariance matrix `var`, we can see that the correlation of neighbouring points is affected by the training data. If a predicted point lies on the training data, there is no correlation with other points. Therefore, the function must pass directly through it. Predicted values further away are also affected by the training data: proportional to their distance.

```python
fig, ax = plt.subplots()
ax.imshow(var, cmap="BuPu", interpolation='None')
ax.axis(False)
```

```python
# functions from new mu and covariance matrix
f_star = np.random.multivariate_normal(mu, var, 100)
f_star_mean = f_star.mean(axis=0)
f_star_std = f_star.std(axis=0)
```

Now we have our function prectitons `f_star`that intercepts all training points

```python
# plot predictions
fig, ax = plt.subplots(figsize=(10,5))
#plt.scatter(x, y, 100, 'k', 'o', zorder=100)
ax.scatter(x_samp, y_samp, s=50, c="C1", zorder=10)
ax.plot(x, f_star.T, color='C0', alpha=0.1)
# if you have altered the function you may need to alter the y lim
ax.set_xlim([-6,6])
ax.set_ylim([-10,15])
ax.set_xlabel("x")
ax.set_ylabel("f(x)")
plt.show()
```

We can plot below the mean prediction function and the standard deviation. Away from training points the standard deviation is much higher. Reflecting the lack of knowledge in these areas.

```python
fig, ax = plt.subplots(figsize=(10,5))
ax.scatter(x_samp, y_samp, s=50, c="C1", zorder=10)
ax.plot(x,y,color='r',alpha=0.2)
ax.plot(x, f_star_mean, color='C0')
ax.fill_between(x.flatten(), f_star_mean+f_star_std, f_star_mean-f_star_std, alpha=0.1, color='C0')
ax.set_xlim([-6,6])
# if you have altered the function you may need to alter the y lim
ax.set_ylim([-10,15])
ax.set_xlabel("x")
ax.set_ylabel("f(x)")
ax.legend(['actual','predicted'])
```

Since the RBF kernel is stationary it will always return to $\mu=0$ in regions further away from observed training data. This decreases the accuracy for predictions that reach further into the past or the future.

# Sea Level Example

Now let's look at an example using Gaussian Process to Predict sea level change using a subset of sea level data from RISeR dataset provided in python numpy arrays by Oliver Pollard.

Example of a global RSL change output for the Last Glacial maximum to present (using the ICE-5G (Peltier et al., 2015) input to a implemented model (Han and Gomez, 2018) of sea level theory (Kendall et al., 2005)).

# Load Data

For this example we're using data provided in python [.npy files](https://towardsdatascience.com/what-is-npy-files-and-why-you-should-use-them-603373c78883) containing relative sea level change at a single point in the southern north sea at 122 (ka). `highstand` data we will use to test our predictions

```python
highstand = np.load("data/highstand_data.npy")
```

We can use 4 parameters provided in `parameter_data.npy` which correspond to the parameters listed below

The Rates of Interglacial Sea-level Change and Responses (RISeR) dataset covers the early interglaical period and can be used to show relative contribution to the Penultimate Glacial Period (PPGM) Max Ice
Volume and Last Interglacial (LIG) Highstnad from 4 parameters from seidment cores

```python
parameters = np.load('data/parameter_data.npy')
```

# Normalise Data

```python
# useful functions
def stack_parameters(*parameters):
    return np.column_stack([[*parameters]]).T

def normalise(parameters):
    normalised_parameters = np.zeros_like(parameters)
    normalisation_values = []
    for index, parameter in enumerate(parameters.T):
        shift = - np.min(parameter)
        scale = np.max(parameter + shift)
        normalised_parameters[:,index] = (parameter + shift)/scale
        normalisation_values.append((shift, scale))

    return normalised_parameters, normalisation_values

```

```python

# move parameter ranges down to improve results
p1 = parameters[:,0] # Bedrock
p2 = parameters[:,1] # Onshore Sediment
p3 = parameters[:,2] # Marine Sediement
p4 = parameters[:,3] # Ice streams
parmlist = ['Bedrock','Onshore Sediment','Marine Sediement','Ice streams']
parameters = stack_parameters(p1, p2, p3, p4)
highstand = highstand.reshape(-1,1)

parameters_norm, parameters_norm_params = normalise(parameters)
highstand_norm, highstand_norm_params = normalise(highstand)
```

# Plot Data

```python
# plot data for parameter
scatter_fig = plt.figure(figsize=[16,10])
for index in range(4):
    ax = scatter_fig.add_subplot(221+index)
    ax.scatter(parameters_norm[:,index], highstand, s=15, c="#71a8ad")
    ax.set_title(f"{str(parmlist[index])}", fontsize=16, fontweight='bold')
    ax.set_ylabel("Highstand (m)")
```

## Plot parameters against other parameters

```python
fig, ax = plt.subplots(nrows=4, ncols=4, figsize=(15,15))
for index_1 in range(4):
    for index_2 in range(4):
        if index_1 == index_2:
            ax[index_1, index_2].axis('off')
            ax[index_1, index_2].text(0.5,0.5,f"{str(parmlist[index_1])}",horizontalalignment="center",
                                      verticalalignment="center", fontsize=15,
                                      fontweight='bold')
        elif index_2 < index_1:
            ax[index_1, index_2].axis('off')

        else:
            ax[index_1, index_2].scatter(parameters_norm[:,index_2], parameters_norm[:,index_1],s=2,c="red")
            ax[index_1, index_2].set_xlim([0,1])
            ax[index_1, index_2].set_ylim([0,1])
```

```python
# Over lay sparse points in red over the points to be predicted in blue
```

```python
predict_coords = np.load("data/predict_points.npy")
```

```python
fig, ax = plt.subplots(nrows=4, ncols=4, figsize=(15,15))
for index_1 in range(4):
    for index_2 in range(4):
        if index_1 == index_2:
            ax[index_1, index_2].axis('off')
            ax[index_1, index_2].text(0.5,0.5,f"{str(parmlist[index_1])}",
                                      horizontalalignment="center",
                                      verticalalignment="center", fontsize=15)
        elif index_2 < index_1:
            ax[index_1, index_2].axis('off')

        else:
            ax[index_1, index_2].scatter(predict_coords[:,index_2], predict_coords[:,index_1],s=2,c="C0", alpha=0.2)
            ax[index_1, index_2].scatter(parameters_norm[:,index_2], parameters_norm[:,index_1],s=2,c="red")
            ax[index_1, index_2].set_xlim([0,1])
            ax[index_1, index_2].set_ylim([0,1])
```

## Define GP flow model

We're going to use the python library [GP flow model](https://gpflow.readthedocs.io/en/master/notebooks/basics/regression.html) to create our guassian process model to creat a more complex model that in our previous example in less lines of code

`k = gpflow.kernels.Matern52(lengthscales=lscale)`  selects a [Matérn covariance function](https://en.wikipedia.org/wiki/Mat%C3%A9rn_covariance_function) for the [GP flow kernel](https://gpflow.readthedocs.io/en/master/notebooks/advanced/kernels.html)

`m = gpflow.models.GPR(data=(X, Y), kernel=k, mean_function=None)` constructs a regression model from data points and the selected kernal.

to inspect the chosen kernel you can run the `print_summary` command which should show you a list of hyperparamers: `variance` and `lengthscale` and will display information about those hyperparamters which will start at default values of 1 and the [transformation](https://en.wikipedia.org/wiki/Rectifier_(neural_networks)#Softplus) applied.

```python
gpflow.config.set_default_summary_fmt("notebook")
k = gpflow.kernels.Matern52(lengthscales=np.ones((len(parameters_norm.T))))
print_summary(k)
```

```python
m = gpflow.models.GPR(data=(parameters_norm, highstand_norm), kernel=k, mean_function=None)
print_summary(m)
```

## Optamization

`opt = gpflow.optimizers.Scipy()` uses the Scipy optimizer, which by default implements the [Limited Memory Broyden–Fletcher–Goldfarb–Shanno (L-BFGS-B)](https://en.wikipedia.org/wiki/Limited-memory_BFGS) algorithm

`opt.minimize(m.training_loss, m.trainable_variables, options=dict(maxiter=100))` calls the minimize method of an optimizer which uses the training_loss defined by the GPflow model and the variables to train with  and the number of iterations

```python
opt = gpflow.optimizers.Scipy()
opt.minimize(m.training_loss, m.trainable_variables, options=dict(maxiter=100))
print_summary(m)
```

## Prediction

`m.predict_f` predicts at the new points producing a mean and variance we'll create an array of predicted means `predict` and variance `predict_var` by looping over the normalised training data from our parameters

```python
mean, var = m.predict_f(np.asarray([[0.2,0.5,0.6,0.7]]))
print(mean.numpy()[0][0], var.numpy()[0][0])
```

```python
mean, var = m.predict_f(predict_coords)
print(mean.numpy()[:,0], var)
```

```python
def coords_flattern(*coords):
    meshs = np.meshgrid(*coords)
    return np.column_stack(([mesh.ravel() for mesh in meshs]))
```

```python
predict = []
predict_var = []
for index, point in enumerate(parameters_norm):
    parameters_norm_training = np.delete(parameters_norm, index, axis=0)
    highstand_norm_training = np.delete(highstand_norm, index, axis=0)

    k = gpflow.kernels.Matern52(lengthscales=np.ones((len(parameters.T))))
    m = gpflow.models.GPR(data=(parameters_norm_training, highstand_norm_training), kernel=k, mean_function=None)
    opt.minimize(m.training_loss, m.trainable_variables, options=dict(maxiter=100))

    lop = parameters_norm[index]
    meanp, varp = m.predict_f(coords_flattern([lop[0]],[lop[1]],[lop[2]],[lop[3]]))
    predict_var.append(varp.numpy()[0][0])
    predict.append(meanp.numpy()[0][0])
```

## Cross Validation

we can now plot our predicted values (`predict`) against our actual values (`highstand`) *normalised*

```python
fig, ax = plt.subplots(figsize=[6,6])
ax.plot(np.linspace(0,1,num=100), np.linspace(0,1,num=100), c="#031f1d")
ax.plot(highstand_norm, predict, 'x', c="#71a8ad")
ax.errorbar(highstand_norm, predict, yerr=predict_var, linestyle="None")
ax.set_ylabel("Predicted Highstand Normalised")
ax.set_xlabel("Actual Highstand Normalised")
```

## Plot High Stands

We can now plot the actual predictions (`mean`) in purple with the grey dots agreemeny with the actual data (`highstand_norm`)

```python
# plot data for parameter
scatter_fig = plt.figure(figsize=[16,10])
for index in range(4):
    ax = scatter_fig.add_subplot(221+index)
    ax.scatter(predict_coords[:,index], mean.numpy()[:,0], s=15, c="purple")
    ax.scatter(parameters_norm[:,index], highstand_norm, s=15, c="#71a8ad")
    ax.set_title(f"{str(parmlist[index])}", fontsize=16, fontweight='bold')
    ax.set_ylabel("Highstand (m)")
    plt.legend(['mean','highstand_norm'])

plt.tight_layout()
```

```python

```


```python
import sympy
from einsteinpy.symbolic import MetricTensor, ChristoffelSymbols, RiemannCurvatureTensor
import astropy.units as u

from einsteinpy import constant
from einsteinpy.utils import scalar_factor as sf
from einsteinpy.utils import scalar_factor_derivative as sfd
from einsteinpy.utils import time_velocity

sympy.init_printing()
```

```python
syms = sympy.symbols('t r theta phi')
# define the metric
k = 1.0
a = sf
t, r = syms[0], syms[1]

den = 1 - k*(syms[1]**2)
metric = [[0 for i in range(4)] for i in range(4)]
metric[0][0] = -1
metric[1][1] = sf
metric[2][2] = a
# metric[3][3] = a**2 * r**2 * sin([syms[2]]**2)

# m_obj = MetricTensor(metric, syms)
# m_obj.tensor()
```

```python

```

```python

```


# Find a best line separating two sets of points.

First some facts about the distance between a point and a line.

```python
import sys
if ".." not in sys.path:
    sys.path.append("..")

import sympy as s
s.init_printing()

x,y,a,b,c = s.symbols("x y a b c")
```

The squared distance between a line

```python
s.Eq(a*x + b*y + c, 0)
```

and the point $(x,y)$ is

```python
distance = (a*x + b*y + c)**2/(a**2 + b**2)
distance
```

We seek to find the parameters $a$, $b$ and $c$.  To use Franke-Wolfe we will need to compute
the gradient for the distance using the partial derivatives

```python
s.diff(distance, a)
```

```python
s.diff(distance, b)
```

```python
s.diff(distance, c)
```

```python
a2b2inv = 1.0/(a**2 + b**2)
axbyc = a * x + b * y + c
da = - (2 * a * (a2b2inv ** 2)) * (axbyc ** 2) + (2 * x * a2b2inv * axbyc)
db = - (2 * b * (a2b2inv ** 2)) * (axbyc ** 2) + (2 * y * a2b2inv * axbyc)
dc = a2b2inv * 2 * axbyc
dc
```

```python
import separate
import numpy as np
s2 = np.sqrt(2)
```

```python
separate.line_illustration(1,-1.2,3,0,2)
```

```python
separate.line_point_distance2(1,1,-3,2,2)
```

```python
red = [(0,1), (0, -3), (-1,2), (4,3), (3,4)]
blue = [(-1, -1), (0,0), (-4,-3), (-3,-4)]
RB = separate.RedBlueObjective(red, blue)
RB.run()
```

    bounds [[-8, 8], [-8, 8], [-8, 8]]
    abc [-8. -8. -8.]

    <IPython.core.display.Javascript object>

    abc [-8.  0. -8.]
    abc [-8.         -0.81927711 -3.18072289]
    abc [-7.         -1.26807229 -3.48192771]
    abc [-7.03125    -1.47844503 -3.12311747]
    abc [-7.09179688 -1.28363258 -2.42792263]
    abc [-6.85598755 -1.38857582 -2.51498634]
    abc [-6.87386274 -1.24187932 -2.60068968]
    abc [-6.75766069 -1.29467714 -2.64287179]
    abc [-6.76251358 -1.25836981 -2.66379807]

```python
from jp_gene_viz import examine
examine.examine(RB.drawing)
```

    <IPython.core.display.Javascript object>

```python
mean = (1,2)
cov = np.diag([3,4])
```

```python
reload(separate)
```

    <module 'separate' from 'separate.py'>

```python
random.multivariate_normal(mean, cov, 20)
```

```python
import time
#time.sleep(10)
RB = separate.mvn_test()
```

    bounds [[-9.804807260934167, 9.804807260934167], [-9.804807260934167, 9.804807260934167], [-9.804807260934167, 9.804807260934167]]
    abc [-9.80480726 -9.80480726 -9.80480726]
    abc [-9.80480726 -4.90240363 -9.80480726]
    abc [-4.90240363 -6.12800454 -9.80480726]
    abc [-5.51520408 -4.13640306 -9.80480726]
    abc [-3.63434968 -3.60435568 -6.85155206]
    abc [-1.94633405 -2.89939297 -6.24320802]
    abc [-2.0691227  -2.70088984 -6.298858  ]
    abc [-2.03726851 -2.69547831 -6.23395901]

```python
RB.limit
```

```python
RB.callback_args[-1]
```

    (            status: 0
                runner: <franke_wolfe.fw_optimize.FrankWolfe object at 0x10dd442d0>
                vertex: array([-19.29258888, -19.29258888, -19.29258888])
               g_count: 9
               message: 'Successful.'
              estimate: array([ -1.68677606,  -3.10850605, -13.10955425])
                   nit: 7
         next_estimate: array([ -1.6867847 ,  -3.10850926, -13.10957783])
                 slack: array([ 38.58517777,  38.58517777,  38.58517777])
               success: True
     convergence_value: 0.0
                 blend: array([ -3.08885044,  -3.63503434, -13.00046312])
             iteration: 7
         scalar_result:    start: array([ -1.68677606,  -3.10850605, -13.10955425])
     message: 'improvement found'
         end: array([ -3.95149411,  -3.95149411, -19.29258888])
     success: True
           x: array([ -1.6867847 ,  -3.10850926, -13.10957783])
                   fun: 0.0
                     x: array([ -1.6867847 ,  -3.10850926, -13.10957783])
             tolerance: 1e-12
               f_count: 89,
     True,
     array([ -1.68677606,  -3.10850605, -13.10955425]),
     array([ 0.,  0.,  0.]),
     array([-19.29258888, -19.29258888, -19.29258888]),
     array([ -1.6867847 ,  -3.10850926, -13.10957783]),
     array([ -3.08885044,  -3.63503434, -13.00046312]))

```python
def f(x):
    x = x - 1
    q = 0.5 * (x ** 2 - 1)
    p = np.where(x>1, x, 1)
    lg = np.log(p)
    return np.where(x>1, lg, q)

def fp(x):
    x = x-1
    p = np.where(x>1, x, 1)
    inv = 1.0 / p
    return np.where(x>1, inv, x)

print fp(np.linspace(-2,2,15))
```

    [-3.         -2.71428571 -2.42857143 -2.14285714 -1.85714286 -1.57142857
     -1.28571429 -1.         -0.71428571 -0.42857143 -0.14285714  0.14285714
      0.42857143  0.71428571  1.        ]

```python
from jp_svg_canvas import cartesian_svg
m = -3
M = 3
D = cartesian_svg.sdoodle(m, m, M, M, html_width=600, html_height=600)
xs = np.linspace(-M,M,100)
ys = f(xs)
fps = fp(xs)
D.sequence(None, xs, ys, "blue")
D.sequence(None, xs, fps, "red")
D.axes()
D.embed()
```

```python
def show_points(*points_arrays):
    colors = ["red", "blue", "green", "purple", "cyan", "magenta"]
    all_points = np.vstack(points_arrays)
    xs = all_points[:,0]
    ys = all_points[:,1]
    mx = xs.min()
    Mx = xs.max()
    my = ys.min()
    My = ys.max()
    radius = max(Mx-mx, My-my) * 0.03
    D = cartesian_svg.sdoodle(mx,my, Mx-mx, My-my, html_width=400)
    D.axes((Mx+mx)*0.5, (My+my)*0.5)
    for (i, points) in enumerate(points_arrays):
        color = colors[i % len(colors)]
        for (x,y) in points:
            D.circle(None, x, y, radius, color)
    D.embed()
    return D
```

```python
points = np.array([[1,0], [1,1],[1,-1],[-1,1],[-1,-1.0],])
a,b,c = -1.0, 0.7, 1.0
xp = 0.0
yp = -c/b
p = np.array([xp, yp])
print a * xp + b * yp + c
print p
D = show_points(points)
```

    0.0
    [ 0.         -1.42857143]

```python
from jp_gene_viz import examine
examine.examine(D)
```

    <IPython.core.display.Javascript object>

```python
tr_points = points - p
show_points(points, tr_points)
```

```python
line_direction = np.array([-b, a])
(xtest, ytest) = p + 12.3 * line_direction
print a * xtest + b * ytest + c, "should be 0"
orth = np.array([a, b])
from numpy.linalg import norm
n = orth/norm(orth)
print norm(n), "should be 1"
n.dot(line_direction)
```

```python
proj = tr_points.dot(n)
proj
```

    array([ 0.        ,  0.57346234, -0.57346234,  2.21192619,  1.0650015 ])

```python
component = proj.reshape((len(points),1)) * n.reshape((1,2))
show_points(points, tr_points, component)
```

```python

```

```python
component.dot(line_direction)
```

    array([  0.00000000e+00,   0.00000000e+00,   5.55111512e-17,
             0.00000000e+00,   0.00000000e+00])

```python
on_line_tr = tr_points - component
on_line = on_line_tr + p
xs = on_line[:,0]
ys = on_line[:,1]
a * xs + b * ys + c
```

    array([  0.00000000e+00,  -2.22044605e-16,   0.00000000e+00,
             1.11022302e-16,   1.11022302e-16])

```python
line_samples = p.reshape((1,2)) + np.linspace(-1,1,10).reshape((10,1)) * line_direction.reshape((1,2))
#show_points(points, tr_points, component, on_line_tr, line_samples, on_line)
show_points(line_samples, on_line, points)
#line_samples
```

```python
s.solve([e1,e2], x, y)
```

```python
a,b,g,m = s.symbols("a b g m")
```

```python
eq1 = s.Eq(a + b, 1 + m**2)
eq1
```

```python
eq2 = s.Eq(a**2 + g**2, 1)
eq2
```

```python
eq3 = s.Eq(b**2 + g**2, m**2)
eq3
```

```python
s.solve([eq1,eq2,eq3], a, b, g)
```

```python

```


# Sandbox/demo to go along with the MD assignment and lectures

## PharmSci 175/275
Author: David Mobley

## Preparation

To use this notebook you will either need to run it locally using Jupyter (with a fortran library you pre-compile) or on Google Colab. This notebook briefly explains either route, though if you are going the local route you may need to compile the library before installing some of the other course materials, or do so in a clean/new conda environment. Some users have reported that conda installing `openforcefield`, `openmm` and the openeye toolkits after installing `gfortran` results in the loss of the ability to compile fortran libraries for use in Python.

### Preparation for Google Colab (NOT FOR LOCAL USE)

[](https://colab.research.google.com/github/MobleyLab/drug-computing/blob/master/uci-pharmsci/lectures/MD/MD_sandbox.ipynb)

For Google Colab, pip installation of software is faster, but we've only been able to get `gfortran` working via a conda installation, so we'll need to go that route. Begin by unsetting the PYTHONPATH to prevent issues with miniconda, then installing miniconda (which will take perhaps 20 seconds to a couple of minutes):

```python
%env PYTHONPATH=
! wget https://repo.anaconda.com/miniconda/Miniconda3-py37_4.10.3-Linux-x86_64.sh
! chmod +x Miniconda3-py37_4.10.3-Linux-x86_64.sh
! bash ./Miniconda3-py37_4.10.3-Linux-x86_64.sh -b -f -p /usr/local
import sys
sys.path.append('/usr/local/lib/python3.7/site-packages/')
```

Once that is done, install `gfortran`, which will take roughly a similar amount of time:

```python
!conda install -c conda-forge libgfortran --yes
```

Next, mount your Google Drive and ensure you have the md_sandbox.f90 file available **at a path you define below**:

```python
from google.colab import drive
drive.mount('/content/drive',force_remount = True)

#EDIT THIS TO DEFINE WHERE YOU PUT THE FILE:
md_library_path = '/content/drive/MyDrive/drug-computing/uci-pharmsci/lectures/MD/'

# Then run:
%cd $md_library_path
```

Then compile the requisite library:

```python
!f2py3 -c -m md_sandbox md_sandbox.f90
```

### Preparation for local use

For local use, you need to compile the fortran module `md_sandbox` for use as a Python library. This accelerates the numerical calculations and a similar framework will be used in the MD and MC assignments. Use `f2py3 -c -m md_sandbox md_sandbox.f90` to compile. (As noted above you may need to do this in advance of installing certain modules, such as the beginning of the course, or by making a clean conda environment.)

## MD for a simple Lennard-Jones system

### Here, let's play with a very simple model system
In the MD assignment, we will be looking at a very simple polymer model. But here, let's backtrack to simple Lennard-Jones spheres (as used in the energy minimization assignment) and look at what happens if we run a simple dynamics calculation on a couple of particles subject to this potential, which will allow us to get a feel for how the integrator works.

Remember, for a Lennard-Jones system, the dimensionless form of our potential is
\begin{equation}
U^* = \sum \limits_{i} 4\left( r_{ij}^{-12} - r_{ij}^{-6}\right)
\end{equation}

which we can easily graph to get a sense of what will happen.

### We graph the potential for a pair of particles:

## Now, let's run some dynamics on a pair of particles subject to this potential

Here I've written a modified `mdlib.f90` (updated from that from the energy minimization and MD assignments) called `md_sandbox.f90`. Compile it as usual (with `f2py`) into `md_sandbox` (e.g. `f2py -c -m md_sandbox md_sandbox.f90` or similar) so that you can import it below.

The difference between this and `mdlib` that you will use in your MD assignment is that I've removed the bonds between atoms, so that we have simple Lennard-Jones particles rather than LJ polymers.

### First, we set up our system:

```python
#Get pylab ready for plotting in this notebook - "magic" command specific for iPython notebooks
%pylab inline
#Import numpy
import numpy as np

#Generate array of distances to graph at
r = np.arange( 0,3, 0.01)

#Calculate U
U = 4.*(r**(-12.) - r**(-6.))

#Graph
plot( r, U, 'b-')
#Label x and y axes
xlabel('r')
ylabel('U')
#Adjust y limits not to be auto-scaled; since this goes to infinity at zero, the graph will not be useful unless we truncate
ylim(-2,2)

```

```python
import md_sandbox
from md_sandbox import *

#Let's define the variables we'll need
dt = 0.001
Cut = 2.5
L = 10 #Let's just put these in a fairly big box so we have room
M = 1 #Here this doesn't actually do anything, but I didn't remove it from the functions

#Choose N for number of particles
N = 2

#Allocate position array - initially just zeros
Pos = np.zeros((N,3), float)

#In this case, place two LJ particles at specified initial positions - chosen so they are relatively near each other
#If you had more than two particles you'd need to adjust this
Pos[0,:] = np.array([0,0,0])
Pos[1,:] = np.array([1.2,0,0])
#Might be worth experimenting to see what happens if we randomly place the particles instead

#Assign initial velocities - in this case I'll start off with them stationary and see what happens
Vel = np.array([[0,0,0],[0,0,0]], float)
#You could tweak the initial velocities to see what happens under different conditions
```

## Before we do any dynamics, notice that this is really a 1D system, so let's write a function to compute r, which we will store later

```python
def get_r(Pos):
    """Calculate r, the distance between particles, for a position array containing just two particles. Return it."""

    #Get displacement
    dist = Pos[1,:] - Pos[0,:]
    #Calculate distance and return
    return np.sqrt( np.dot( dist, dist))
```

## OK, now let's start doing some dynamics

We're going to want to store the distance between the two particles as a function of time, so we can graph it. We also might want to look at the population of each distance as a function of time and see how that compares with the energy landscape. Though, right now that's a little premature. Let's just start off by taking a few timesteps and see how the distance changes.

```python
#Define storage for positions at each time so we can track them
max_steps = 5000 #Maximum number of steps we will take - so we know how many positions we might store
Pos_t = np.zeros(( N,3,max_steps), float)

#Store initial positions
Pos_t[:,:,0] = Pos

#Make up initial forces
Forces = np.zeros((N,3), float)

#Kick things off by calculating energy and forces
energy, Forces = calcenergyforces( Pos, M, L, Cut, Forces )

#Take a timestep
Pos, Vel, Accel, KEnergy, PEnergy = vvintegrate( Pos, Vel, Forces, M, L, Cut, dt )

#Store new positions
Pos_t[:,:,1] = Pos

#Print original and current distance
for i in range(0,2):
    print(get_r(Pos_t[:,:,i]))
```

## What should the long-time behavior of this system be?

Before going on to the step below, think for a minute about what motion these particles should exhibit on long timescales and what it would look like if you've graphed it.

Once you've done so, write a `for` loop to run over max_steps and at each step, update the energy and forces, take a timestep, and store the new positions.

```python
for i in range(max_steps):
   #Your code goes here
    energy, Forces = calcenergyforces( Pos, M, L, Cut, Forces )
    Pos, Vel, Accel, KEnergy, PEnergy = vvintegrate( Pos, Vel, Forces, M, L, Cut, dt )
    Pos_t[:,:,i] = Pos

```

## Once you've done that, use this code to graph r versus time for your particles

```python
#Find x axis (time values)
t = dt*np.arange(0,max_steps)
#Find y axis (r values)
r_vs_t = []
for i in range(max_steps):
    r=get_r(Pos_t[:,:,i])
    r_vs_t.append(r)

r_vs_t = np.array(r_vs_t)

#Plot
figure()
plot(t, r_vs_t)

```

## Additional exercises

Some things you might want to try on the above 1D system:
- Change the amount of time the data is graphed for; see if you can make it oscillate repeatedly
- Consider how you could adjust the total energy of the system
- What if you change the total energy (increasing it or reducing it). Can you make it look more like a harmonic oscillator? Less like a harmonic oscillator? Why or why not?
- Can you find solutions which are not oscillatory? How?
- For the ambitious: Consider extending this example to three or more particles and graph just some of the distances involved. What kind of solutions can you find?


# EXERCISE (Gravity)

Create a plot that shows the vertical gravity above the center of a circular anomaly for different heights $h$
above the center of the anomaly. Test heights starting from $h=120m$ up to a height of $h=300$ in increments
of $20m$.

** Hint**: Modify the code in EscriptBasics.ipynb but you need create a new `Locator`.

# EXERCISE (Gravity)

Plot the vertical gravity along a transect at a height of about 150m above the horizontal center line for
a gravity anomaly of $\rho=1 \frac{t}{m^3}$ from an interface formed by a horizontal harmonic varying
vertically with an amplitude of $100m$ at a mean depth of $100m$ below the horizontal center line of the domain.
So the interface is located at
\begin{equation}\label{eq:I}
z = \bar{z} + 100m \cdot sin( k \cdot x)
\end{equation}
where $\bar{z}$ is the mean depth.
Domain size is $1km \times 1km$ with a grid spacing of $10m$. Try different wave numbers $k$.

# EXERCISE (Magnetic)

Plot the total magnetic field anomaly over the a transect for vertical dyke for Beijing.
Modify the script in Magnetic Anomalies

** Hint **  Use the [Magnetic Field Calculator](https://www.ngdc.noaa.gov/geomag/calculators/magcalc.shtml#igrfwmm) to get the background magnetic field.

# EXERCISE (Magnetic)

Calculate the total magnetic field anomaly over a transect for dyke with a dip other than $90^o$ (non-vertical)

# EXERCISE (MT)

Write a program that plots the apparent resistivity and the phase for frequencies for frequencies
$f$ between $0.01$ and $10 Hz$ in logarithmic increments in the corresponding plots.
The best strategy is to write a function `getImpedance` that takes a frequency as an argument and returns
apparent resistivity and the phase along the transect for this frequency.

# EXERCISE (MT)

For $E$-polarization (TE-mode) the unknown is the transversal electric field $E_x$.
The corresponding 2D simplification of the Maxwell's equations for this case are
given as
\begin{equation}\label{eqTEHz}
\frac{\partial E_x}{\partial y }=  \mathbf{j} \omega \mu_0  H_z
\end{equation}
\begin{equation}\label{eqTEHy}
\frac{\partial E_x}{\partial z }=   - \mathbf{j} \omega \mu_0  H_y
\end{equation}
The horizontal components then need to fulfill the equation:
\begin{equation}\label{eqMaxwellTE}
\frac{\partial H_z}{\partial y }  - \frac{\partial H_y}{ \partial z } = \sigma  E_x
\end{equation}
In the TE mode the impedance is given as
\begin{equation}\label{eqImpTE}
Z_{xy}(\omega)=\frac{E_x}{H_y}
\end{equation}
Write a program that solves these equations for the conductive step for a given frequency (eg. f= $1Hz$)
and plot the apparent resistivity and the phase along the transect for this frequency.

# EXERCISE (Wave in Frequency Domain)
Starting from the code in Waves in Frequency Domain
add an additional layer of with a speed $c_{mid}=2500 \frac{m}{s}$ between the top and bottom layer.
You may need to adjust the thickness of the layers.

# EXERCISE (Wave in Frequency Domain)
Starting from the code in Waves in Frequency Domain or the code you have developed in the previous exercise
add a second source at an offset of $1000m$ from the left boundary names "source60" and create an amplitude vs offset plot for this source. Plot the amplitude profile for "source60" and "source66" in the same plot.

# EXERCISE (Wave in Frequency Domain)
Using from the code in Waves in Frequency Domain or the code you have developed in the previous exercise plot the phase profiles for "source66" and "source60".
** Hint** use the `phase` function in `esys.escript`

#  EXERCISE (Wave in Time Domain)¶

Starting from the code in Waves in Frequency Domain
add an additional layer of with a speed $c_{mid}=2500 \frac{m}{s}$ between the top and bottom layer.
You may need to adjust the thickness of the layers.

#  EXERCISE (Wave in Time Domain)¶

Starting from the code in Waves in Frequency Domain
Increase the Ricker peak frequency. You may need to reduce the spacing and time steping in order to keep the solution
stable.