```
%matplotlib inline
```

# Problem 1

Find an analytic expression for the symbol P(f) of the B‐L wavelet filter. Plot |P(f)| for –1/2 < f < +1/2

$$
\begin{aligned}
N_{1}(f)&=30 \sin^{2}{\left (\pi f \right )} \cos^{2}{\left (\pi f \right )} + 30 \cos^{2}{\left (\pi f \right )} + 5\\
N_{2}(f)&=2 \sin^{4}{\left (\pi f \right )} \cos^{2}{\left (\pi f \right )} + 70 \cos^{4}{\left (\pi f \right )}\\
S(f)&=\frac{1}{105 \sin^{8}{\left (\pi f \right )}} \left(2 \sin^{4}{\left (\pi f \right )} \cos^{2}{\left (\pi f \right )} + 30 \sin^{2}{\left (\pi f \right )} \cos^{2}{\left (\pi f \right )} + 70 \cos^{4}{\left (\pi f \right )} + 30 \cos^{2}{\left (\pi f \right )} + 5\right)\\
\hat{\phi}(f)&=\frac{\sqrt{105}}{\pi^{4} f^{4} \sqrt{\frac{1}{\sin^{8}{\left (\pi f \right )}} \left(2 \sin^{4}{\left (\pi f \right )} \cos^{2}{\left (\pi f \right )} + 30 \sin^{2}{\left (\pi f \right )} \cos^{2}{\left (\pi f \right )} + 70 \cos^{4}{\left (\pi f \right )} + 30 \cos^{2}{\left (\pi f \right )} + 5\right)}}\\
\end{aligned}$$

$$P(f)=\frac{\hat{\phi}(2f)}{\hat{\phi}(f)}=\frac{\sqrt{\frac{1}{\sin^{8}{\left (\pi f \right )}} \left(2 \sin^{4}{\left (\pi f \right )} \cos^{2}{\left (\pi f \right )} + 30 \sin^{2}{\left (\pi f \right )} \cos^{2}{\left (\pi f \right )} + 70 \cos^{4}{\left (\pi f \right )} + 30 \cos^{2}{\left (\pi f \right )} + 5\right)}}{16 \sqrt{\frac{1}{\sin^{8}{\left (2 \pi f \right )}} \left(2 \sin^{4}{\left (2 \pi f \right )} \cos^{2}{\left (2 \pi f \right )} + 30 \sin^{2}{\left (2 \pi f \right )} \cos^{2}{\left (2 \pi f \right )} + 70 \cos^{4}{\left (2 \pi f \right )} + 30 \cos^{2}{\left (2 \pi f \right )} + 5\right)}}$$

```
import matplotlib.pyplot as plt
import numpy as np
import sympy as sy
import utilities.wavelet as wv

f = sy.symbols('f')
symbol = wv.battle_lemarie_symbol()

# plot symbol
symbol_lambda = sy.lambdify(f, symbol, 'numpy') # lambdify for plotting
freq = np.linspace(-0.5,0.5)
plt.figure()
plt.title('B-L Symbol')
plt.xlabel('frequency')
plt.plot(freq, [np.abs(symbol_lambda(v)) for v in freq])
plt.show()
```

# Problem 2

Calculate the B-L wavelet filter coefficients $h_k$ for -11 < k < 11.

$$
\begin{aligned}
P(f)&=1/\sqrt{2}\sum_{-\infty}^{\infty}h_{k}e^{-i2\pi kf}\\
\sqrt{2}P(f)&=\sum_{-\infty}^{\infty}h_{k}e^{-i2\pi kf}\\
\end{aligned}
$$

The filter coefficents are the fourier coefficients of $\sqrt{2}P(f)$

## Wavelet Filter Coefficient

```
import matplotlib.pyplot as plt
import numpy as np
import sympy as sy
from scipy.signal.wavelets import cascade
import utilities.wavelet as wv

def f_coef(exp, period, n):
    """
    Find fourier coefficients of expression.

    Args:
        exp (function): symbolic function
        period (float): period of symbolic function
        n (int): number of coefficients to compute

    Returns:
        dc (float): dc coefficient
        hp (float): positive coefficients
        hn (float): negative coefficients
    """
    period = float(period)

    # compute delta time
    dt = period/n

    x_sample = np.zeros([n])
    for k in range(n):
        if k == 0:
            x_sample[k] = exp(0 + np.finfo(float).eps)
        else:
            x_sample[k] = exp(k*dt)

    coef = np.fft.fft(x_sample)/n

    dc = coef[0]
    hp = coef[1:n/2+1]
    hn = coef[-n/2+1:]

    return dc, hp, hn

f = sy.symbols('f')
symbol = wv.battle_lemarie_symbol()
symbol_lambda = sy.lambdify(f, symbol)
exp = lambda f: np.sqrt(2)*symbol_lambda(f)
n_coef = 23
dc, hp, hn = f_coef(exp=exp, period=1, n=n_coef)

print 'dc:'
print np.real(dc)
print 'hp:'
for v in hp:
    print np.real(v)
print 'hn:'
for v in hn:
    print np.real(v)
```

    dc:
    0.775971513219
    hp:
    0.4349719571
    -0.0590042181291
    -0.112420960149
    0.0384344633754
    0.0443591447983
    -0.020884809629
    -0.0192461669425
    0.0100985636138
    0.00799378004201
    -0.00351493706705
    -0.00166579243544
    hn:
    -0.00166579243544
    -0.00351493706705
    0.00799378004201
    0.0100985636138
    -0.0192461669425
    -0.020884809629
    0.0443591447983
    0.0384344633754
    -0.112420960149
    -0.0590042181291
    0.4349719571

# Problem 3

Find an analytic expression for the Fourier Transform of the B‐L wavelet. Plot it’s absolute value.

$$
\begin{align}
\phi(t)	&\rightarrow\hat{\phi}(\xi)\\
\phi(t-k)	&\rightarrow e^{-i2\pi k\xi}\hat{\phi}(\xi)\\
\phi(2t-k)	&\rightarrow\frac{1}{2}e^{-i2\pi k\xi}\hat{\phi}(\frac{\xi}{2})\\
\hat{\psi}(\xi)	&=\left[\sum(-1)^{k}\bar{h}_{1-k}\frac{\sqrt{2}}{2}e^{-i\pi k\xi}\right]\hat{\phi}(\frac{\xi}{2})\\
l	&=1-k\\
k	&=1-l\\
\hat{\phi}(\xi)	&=\frac{-e^{-i\pi\xi}}{\sqrt{2}}\sum_{l}(-1)^{-l}\hat{h}_{l}e^{i\pi l\xi}\\
\hat{\phi}(\xi)	&=\frac{-e^{-i\pi\xi}}{\sqrt{2}}\sum_{l}(-1)^{l}\hat{h}_{l}e^{i\pi l\xi}\\
\hat{\phi}(\xi)	&=\frac{-e^{-i\pi\xi}}{\sqrt{2}}\sum_{l}e^{i\pi l}\hat{h}_{l}e^{i\pi l\xi}\\
\hat{\phi}(\xi)	&=\frac{-e^{-i\pi\xi}}{\sqrt{2}}\sum_{l}\hat{h}_{l}e^{i\pi l(1+\xi)}\\
\hat{\phi}(\xi)	&=-e^{-i\pi\xi}\frac{1}{\sqrt{2}}\sum h_{l}e^{-2\pi il\left(\frac{1+f}{2}\right)}\\
\hat{\phi}(\xi)   &=-e^{-i\pi\xi}P\left(\frac{1+\xi}{2}\right)\\
\hat{\psi}(\xi)	&=-e^{-i\pi\xi}\overline{P\left(\frac{1+\xi}{2}\right)}\hat{\phi}\left(\frac{\xi}{2}\right)\\
\end{align}
 $$

```
import sympy as sy
import numpy as np
import matplotlib.pyplot as plt
import utilities.wavelet as wv

f = sy.symbols('f')
psi_hat = wv.battle_lemarie_wavelet_transform()
psi_hat_lambda = sy.lambdify(f, psi_hat, 'numpy')

freq = np.linspace(-2,2, 100)
freq[0] = np.finfo(float).eps
psi_hat_signal = np.array(np.abs([psi_hat_lambda(v) for v in freq]))

plt.figure()
plt.title('Fourier Transform of Mother Wavelet')
plt.plot(freq, psi_hat_signal)
plt.show()
```

# Problem 4

Find and plot the B‐L scaling function and mother wave.

```
import copy
import matplotlib.pyplot as plt
import sympy as sy
import numpy as np
import utilities.wavelet as wv

def f_trans(x, F, N, M):
    """
    Discrete Fourier Transform (frequency to time)

    x (symbolic): symbolic function of f
    F (float): frequency range
    N (int): number of points
    M (int): number of aliases
    """
    f = sy.symbols('f')

    dt = 1/F
    df = F/N
    T = N/F

    xp = copy.deepcopy(x)

    for k in range(1, M+1):
        xp = xp+x.subs(f,f-k*F)+x.subs(f,f+k*F)

    # lambdify symbolic function
    xp = sy.lambdify(f, xp, 'numpy')

    xps = np.zeros([N])
    ts = np.zeros([N])
    for n in range(N):
        if n == 0:
            xps[n] = xp(np.finfo(float).eps)
            ts[n] = -T/2
        else:
            xps[n] = xp(n*df)
            ts[n] = n*dt - T/2

    Xs = np.fft.fft(xps)*df
    Xs = np.fft.fftshift(Xs)

    return Xs,ts

# Convert scaling function to time domain
phi_hat = wv.battle_lemarie_scaling_transform()
Xs, ts = f_trans(phi_hat, 8.1, 128, 0)

# Plot scaling function
plt.figure()
plt.subplot(1,2,1)
plt.title('Scaling Function')
plt.plot(ts, Xs)

# Convert wavelet to time domain
psi_hat = wv.battle_lemarie_wavelet_transform()
Xs, ts = f_trans(psi_hat, 8.1, 128, 0)

# Plot wavelet
plt.subplot(1,2,2)
plt.title('Mother Wavelet')
plt.plot(ts, Xs)
plt.show()
```

# Problem 5

Compress your image using the CDF2.4 analysis and synthesis filters given in the Matlab script “qmfilter.m”. Create corresponding high‐pass filters. Encode 4 levels. Set cutoff to .98, quantize with 8 bits, gzip, calculate compression ratio, reverse procedure, and obtain recreated image.  Display original and recreated images.

```
import copy
import matplotlib.pyplot as plt
import matplotlib.cm as cm
import numpy as np
import os
import subprocess
import utilities.image as im
import utilities.wavelet as wv
import utilities.quantization as quant

img = im.read_gecko_image()
plt.figure()
plt.subplot(1,3,1)
plt.title('Original Image')
plt.imshow(img, cm.Greys_r)

# Forward Transform
img_fwd = copy.deepcopy(img)
dim = max(img.shape)
while dim >= 8:
    P = wv.permutation_matrix(dim)
    T_a = wv.cdf_24_encoding_transform(dim)
    img_fwd[:dim,:dim] = P.dot(T_a).dot(img_fwd[:dim,:dim]).dot(T_a.T).dot(P.T)
    dim = dim / 2

plt.subplot(1,3,2)
plt.title('Transformed Image')
plt.imshow(img_fwd, cm.Greys_r)

# Threshold + Encode
t, ltmax = quant.log_thresh(img_fwd, cutoff=0.98)
img_encode = quant.encode(img_fwd, t, ltmax)

# Store to file
filename = 'encoded_image'
img_encode.tofile(filename)
file_size = os.stat(filename).st_size

# Compress File
subprocess.call(['gzip', filename])
c_file_size = os.stat(filename + '.gz').st_size

# Decompress File
subprocess.call(['gunzip', filename + '.gz'])

# # Read from file
img_encode = np.fromfile(filename).reshape(img.shape)

# Decode Image
img_decode = quant.decode(img_encode, t, ltmax)

# Inverse Transform
img_inv = copy.deepcopy(img_decode)
dim = 8
while dim <= max(img.shape):
    P = wv.permutation_matrix(dim)
    T_b = wv.cdf_24_decoding_transform(dim)
    img_inv[:dim,:dim] = T_b.T.dot(P.T).dot(img_inv[:dim,:dim]).dot(P).dot(T_b)
    dim = dim * 2

plt.subplot(1,3,3)
plt.title('Recreated Image')
plt.imshow(img_inv, cm.Greys_r)
plt.show()

print "Compression Level: %s" % (1 - float(c_file_size) / float(file_size))
```


# Lecture 11: Matrix functions

## Syllabus
**Week 1:** Python intro
**Week 2:** Matrices, vectors, norms, ranks
**Week 3:** Linear systems, eigenvectors, eigenvalues
**Week 4:** Singular value decomposition + test + homework seminar
**Week 5:** Sparse & structured matrices
**Week 6:** Iterative methods, preconditioners, matrix functions
**Week 7:** Test week
**Week 8:** App Period

##Recap of the previous lecture
- Concept of iterative methods
- Richardson iteration, Chebyshev acceleration
- Idea of Krylov methods
- Conjugate gradient methods for symmetric positive definite matrices
- Generalized minimal residual methods
- The Zoo of iterative methods: BiCGStab, QMR, IDR, ...
- The idea of preconditioners
- Jacobi/Gauss-Seidel methods Incomplete ILU

## Today lecture
Today we will talk about **matrix functions**.
- What is a matrix function
- Matrix exponential
- (Some) applications

Book to read: [Nick Higham, Functions of matrices](http://www.google.ru/books?hl=ru&lr=&id=2Wz_zVUEwPkC&oi=fnd&pg=PR3&dq=Higham+matrix+function&ots=pTt6fpLGRX&sig=DgUuX-SpBZGin8CFUo-4MYnOcHE&redir_esc=y#v=onepage&q=Higham%20matrix%20function&f=false)

## Where matrix functions come from

- Solution of PDEs and ODEs (matrix exponential, matrix sine function, matrix cosine function, square root,
  sign function)
- Matrix exponential has application in localized PageRank, graph centrality
- Quantum physics
- Inverse is matrix function as well!

## What is a matrix function?
A matrix function is a function of a matrix. A matrix function **is not** a function, applied to the elements!

I.e., $B = \sqrt{A}$ **does not mean** that $B_{ij} = \sqrt{A}_{ij}$.

```
import numpy as np
import scipy.linalg as sla

A = 2 * np.eye(2)
A = np.array([[4, 4], [4, 4]])
sla.funm(A, lambda x: np.sqrt(x) )
```

    array([[ 1.41421356,  1.41421356],
           [ 1.41421356,  1.41421356]])

## The simplest matrix function: matrix polynomial

It is very easy to define a matrix polynomial as
$$
 P(A) = \sum_{k=0}^n c_k A^k.
$$
**Side-note:** [Hamilton-Cayley theorem](https://en.wikipedia.org/wiki/Cayley%E2%80%93Hamilton_theorem) states that $F(A) = 0$ where $F(\lambda) = \det(A - \lambda I)$.

## Matrix polynomials as building blocks
We can define a function of the matrix by **Taylor series**:
$$
   f(A) = \sum_{k=0}^{\infty} c_k A^k.
$$
The convergence is understood as the convergence in some **matrix norm**.
Example of such series is the **Neumann series**
$$
  (I - F)^{-1} = \sum_{k=0}^{\infty} F^k,
$$
which is well defined for $\rho(F) < 1$.

## Matrix exponential series
The most well-known matrix function is **matrix exponential**. In the scalar case,
$$
   e^x = 1 + x + \frac{x^2}{2} + \frac{x^3}{6} + \ldots = \sum_{k=0}^{\infty} \frac{x^k}{k!},
$$
and it directly translates to the matrix case:
$$
    e^A = \sum_{k=0}^{\infty} \frac{A^k}{k!},
$$
the series that always converges (why?).

## Why matrix exponential is important
A **lot of** practical problems are reduced to a system of linear ODEs of the form
$$
   \frac{dy}{dt} = Ay, \quad y(0) = y_0.
$$
The formal solution is given by $y(t) = e^{At} y_0$, so if we know
$e^{At}$ (or can compute matrix-by-vector product fast) there is a big gain over the
time-stepping schemes.

## How to compute matrix functions?

See [C. Van Loan, C. Moler, Nineteen Dubious Ways to Compute the Exponential of a Matrix, Twenty-Five Years Later](http://www.cs.cornell.edu/cv/researchpdf/19ways+.pdf)

The simplest way is to diagonalize the matrix:
$$
  A = S \Lambda S^{-1}.
$$
where the columns of $S$ are **eigenvectors** of the matrix $A$,  then

$$
   F(A) = S F(\Lambda) S^{-1}.
$$
Problem: **diagonalization can be unstable!** (and not every matrix is diagonalizable)

Let us look how matrices are diagonalizable:

```
import numpy as np
eps = 0.5
p = 4
a = np.eye(p)
for i in xrange(p-1):
    a[i, i+1] = 1

a[p-1, 2] = eps
a = np.array(a)
val, vec = np.linalg.eig(a)
#print a
print np.linalg.norm(a - vec.dot(np.diag(val)).dot(np.linalg.inv(vec)))
#print 'S * D * S^{-1}:'
#print vec.dot(np.diag(val)).dot(np.linalg.inv(vec))
print a
```

    2.2360679775
    [[ 1.   1.   0.   0. ]
     [ 0.   1.   1.   0. ]
     [ 0.   0.   1.   1. ]
     [ 0.   0.   0.5  1. ]]

Now we can compute a function

```
import numpy as np
eps = 1e-10
p = 5
a = np.eye(p)
for i in xrange(p-1):
    a[i, i+1] = 1

a[p-1, 0] = eps
a = np.array(a)
val, vec = np.linalg.eig(a)
print np.linalg.norm(a - vec.dot(np.diag(val)).dot(np.linalg.inv(vec)))

fun = lambda x: np.exp(x)

#Using diagonalization
fun_diag = vec.dot(np.diag(fun(val))).dot(np.linalg.inv(vec))

#Using Schur
import scipy
fun_m = scipy.linalg.expm(a)
np.linalg.norm(fun_m - fun_diag)
```

    4.84290066277e-08

    1.3563815727960955e-07

## How funm function works
The exponential of a matrix is a special function, so there are special methods for its computation.  For a general function,
there is a beautiful **Schur-Parlett algorithm**, which is based on the **Schur theorem**

## Schur-Parlett algorithm

Given a matrix $A$ we want to compute $F(A)$, and we only can evaluate $F$ at **scalar points**.
First, we reduce $A$ to the **triangular form** as
$$
   A = U T U^*.
$$

Therefore,  $F(A)=U F(T) U^*$

## Computing functions of triangular matrices
We know values on the diagonals:
and $$F_{ii} = F(T_{ii}),$$
and also we know that
$$F T = T F,$$
and we get

$$f_{ij} = t_{ij} \frac{f_{ii} - f_{jj}}{t_{ii} - t_{jj}} + \sum_{k=i+1}^{j-1} \frac{f_{ik} t_{kj} - t_{ki}f_{kj}}{t_{ii} - t_{jj}}.$$

## Pade approximations
Matrix exponential is well approximated by **rational function**:

$$\exp(x) \approx \frac{p(x)}{q(x)},$$
where $p(x)$ and $q(x)$ are polynomials
and computation of a rational function of a matrix is reduced to **matrix-matrix products** and **matrix inversions**.
The rational form is also very useful when only a product of a matrix exponential by vector is needed, since
evaluation reduces to **matrix-by-vector products** and **linear systems solvers**

A short demo on Pade approximation via **sympy** package

```
import sympy.mpmath
%matplotlib inline
from sympy.mpmath import pade, taylor, polyval
import matplotlib.pyplot as plt
x = np.linspace(0, 1, 128)
a = taylor(sympy.mpmath.exp, 0, 100)
p, q = pade(a, 4, 2)
plt.plot(x, polyval(p[::-1], x)/polyval(q[::-1], x) - np.exp(x))
```

## Application to graph centrality

One of the recent application is the computation. This example was taken from [here](http://nbviewer.ipython.org/github/sdrelton/matrix_function_notebooks/blob/master/TheMatrixExponential.ipynb).
Take the following network.

```
import networkx as nx
import numpy as np
%matplotlib inline
Adj = np.array([[0, 1, 1, 0, 0, 0],
                [1, 0, 1, 1, 1, 0],
                [1, 1, 0, 0, 0, 0],
                [0, 1, 0, 0, 0, 1],
                [0, 1, 0, 0, 0, 0],
                [0, 0, 0, 1, 0, 0]])

G = nx.from_numpy_matrix(Adj)
nx.draw(G, node_color='y', node_size=1000, with_labels=True )
```

One measure of the importance of each node is its <b>centrality</b>.
We can count the number of paths of different lengths from $i$ to $i$,
and add them up to get **centrality** of the node
$$c(i) = \alpha_1 A_{ii} + \alpha_2 A^2_{ii} + \alpha_3 A^3_{ii} + \cdots,$$
where the coefficients $\alpha_k$ remain to be chosen.
With $\alpha_k = \frac{1}{k!}$ we get
$$
    c = \mathrm{diag}(\exp(A))
$$

```
centralities = np.diag(sla.expm(np.array(Adj, dtype=np.double)))
nodeorder = np.argsort(centralities)[::-1]

print np.array([nodeorder, centralities[nodeorder]])

# Note: This is already built into networkx using the following command
# print nx.communicability_centrality_exp(G)
```

    [[ 1.          0.          2.          3.          4.          5.        ]
     [ 4.44723536  2.86427609  2.86427609  2.36018456  1.71615913  1.59432922]]

## Some other matrix functions

- Sign function: $F(A) = \mathrm{sign}(A)$ (no Taylor series), used for spectrum partititioning problems.
  Iteration to compute sign-function: $$A_{k+1} = \frac{1}{2} (A_k + A^{-1}_k), A_0 = A$$
- Cosine/sine functions, used to solve $$\frac{d^2 y}{dt^2} = A y$$
- Square root $\sqrt{A}$, used to solve Lyapunov equations
    $$A X + X A^{\top} = B,$$
     and Lyapunov equation is used in control theory, model order reduction and many other things.

## Take home message
- Matrix exponential
- Schur-Parlett
- Pade approximation
- Graphs & PDEs

## Next lecture
We are done!
Friday - homework seminar

##### Questions?

```
from IPython.core.display import HTML
def css_styling():
    styles = open("./styles/custom.css", "r").read()
    return HTML(styles)
css_styling()
```

<link href='http://fonts.googleapis.com/css?family=Fenix' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Alegreya+Sans:100,300,400,500,700,800,900,100italic,300italic,400italic,500italic,700italic,800italic,900italic' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Source+Code+Pro:300,400' rel='stylesheet' type='text/css'>
<style>
    @font-face {
        font-family: "Computer Modern";
        src: url('http://mirrors.ctan.org/fonts/cm-unicode/fonts/otf/cmunss.otf');
    }
    div.cell{
        /*width:80%;*/
        /*margin-left:auto !important;
        margin-right:auto;*/
    }
    h1 {
        font-family: 'Alegreya Sans', sans-serif;
    }
    h2 {
        font-family: 'Fenix', serif;
    }
    h3{
		font-family: 'Fenix', serif;
        margin-top:12px;
        margin-bottom: 3px;
       }
	h4{
		font-family: 'Fenix', serif;
       }
    h5 {
        font-family: 'Alegreya Sans', sans-serif;
    }
    div.text_cell_render{
        font-family: 'Alegreya Sans',Computer Modern, "Helvetica Neue", Arial, Helvetica, Geneva, sans-serif;
        line-height: 1.2;
        font-size: 160%;
        /*width:70%;*/
        /*margin-left:auto;*/
        margin-right:auto;
    }
    .CodeMirror{
            font-family: "Source Code Pro";
			font-size: 90%;
    }
/*    .prompt{
        display: None;
    }*/
    .text_cell_render h1 {
        font-weight: 200;
        font-size: 50pt;
		line-height: 110%;
        color:#CD2305;
        margin-bottom: 0.5em;
        margin-top: 0.5em;
        display: block;
    }
    .text_cell_render h5 {
        font-weight: 300;
        font-size: 16pt;
        color: #CD2305;
        font-style: italic;
        margin-bottom: .5em;
        margin-top: 0.5em;
        display: block;
    }

    li {
        line-height: 110%;
    }
    .warning{
        color: rgb( 240, 20, 20 )
        }
</style>

```

```


# Problem 2: The AS-AD Model

Consider the following **AS-AD model**. The **goods market equilibrium** is given by

$$ y_{t} = -\alpha r_{t} + v_{t} $$

where $y_{t}$ is the **output gap**, $r_{t}$ is the **ex ante real interest** and $v_{t}$ is a **demand disturbance**.

The central bank's **Taylor rule** is

$$ i_{t} = \pi_{t+1}^{e} + h \pi_{t} + b y_{t}$$

where $i_{t}$ is the **nominal interest rate**, $\pi_{t}$ is the **inflation gap**, and $\pi_{t+1}^{e}$ is the **expected inflation gap**.

The **exante real interest rate** is given by

$$ r_{t} = i_{t} - \pi_{t+1}^{e} $$

Together, the above implies that the **AD-curve** is

$$ \pi_{t} = \frac{1}{h\alpha}\left[v_{t} - (1+b\alpha)y_{t}\right]$$

Further, assume that the **short-run supply curve (SRAS)** is given by

$$ \pi_{t} = \pi_{t}^{e} + \gamma y_{t} + s_{t}$$

where $s_t$ is a **supply disturbance**.

**Inflation expectations are adaptive** and given by

$$ \pi_{t}^{e} = \phi\pi_{t-1}^{e} + (1-\phi)\pi_{t-1}$$

Together, this implies that the **SRAS-curve** can also be written as

$$ \pi_{t} = \pi_{t-1} + \gamma y_{t} - \phi\gamma y_{t-1} + s_{t} - \phi s_{t-1} $$

The **parameters** of the model are:

```python
# Setting parameters from Exam
par = {}

par['alpha'] = 5.76
par['h'] = 0.5
par['b'] = 0.5
par['phi'] = 0
par['gamma'] = 0.075
```

```python
import numpy as np
from scipy import linalg
from scipy import optimize
import sympy as sm
import matplotlib.pyplot as plt
import ipywidgets as widgets
import sympy as sm
```

## Question 1
Use the ``sympy`` module to solve for the equilibrium values of output, $y_t$, and inflation, $\pi_t$, (where AD = SRAS) given the parameters ($\alpha$, $h$, $b$, $\alpha$, $\gamma$) and $y_{t-1}$ , $\pi_{t-1}$, $v_t$, $s_t$, and $s_{t-1}$.

```python
# Defining the AD and the SRAS functions in Sympy

# i. Defining symbols
sm.init_printing(use_unicode=True)
pit = sm.symbols('pi_t')
pit1 = sm.symbols('pi_t-1')
alpha = sm.symbols('alpha')
gamma = sm.symbols('gamma')
phi = sm.symbols('phi')
h = sm.symbols('h')
b = sm.symbols('b')
vt = sm.symbols('v_t')
yt = sm.symbols('y_t')
yt1 = sm.symbols('y_t-1')
st = sm.symbols('s_t')
st1 = sm.symbols('s_t-1')

# ii. AD-curve
AD = sm.Eq(1/(h*alpha)*(vt-(1+b*alpha)*yt), pit)

# iii. SRAS-curve
SRAS = sm.Eq(pit1 + gamma*yt - phi*gamma*yt1 + st - phi*st1, pit)

```

```python
# Solving for y through Sympy

# a. Setting AD equal to SRAS
AD_SRAS = sm.Eq(1/(h*alpha)*(vt-(1+b*alpha)*yt), pit1 + gamma*yt - phi*gamma*yt1 + st - phi*st1)

# b. Solving for y
yt_star = sm.solve(AD_SRAS, yt)

# c. Printing y*
print(f'The equilibrium value of y is:')
yt_star
```

```python
# Solving for pi through Sympy

# a. Inserting y* in the AD function
pit_star = 1/(h*alpha)*(vt-(1+b*alpha)*yt_star[0])

# b. Printing pi*
print(f'The equilibrium value of pi is:')
pit_star
```

## Question 2
Find and illustrate the equilibrium when $y_{t-1} = \pi_{t-1} = v_t = s_t = s_{t-1} = 0$. Illustrate how the equilibrium changes when instead $v_t = 0.1$.

```python
# Defining initial variable values
yt1_int  = 0
pit1_int = 0
vt_int   = 0
st_int   = 0
st1_int  = 0
vt_one   = 0.1

## Solving numerically for y

# a. Defining a lambda function of yt_eq
_sol_y_func = sm.lambdify((h,b,alpha,gamma,phi,vt,st,pit1,yt1,st1),yt_star[0])

# b. Defining a function which solve for y, given values and parameters
def sol_y_func(vt,st,pit1,yt1,st1,h=par['h'],alpha=par['alpha'],b=par['b'],gamma=par['gamma'],phi=par['phi']):
    return _sol_y_func(h,b,alpha,gamma,phi,vt,st,pit1,yt1,st1)

# c. Solving for y and printing the solution
y_star = sol_y_func(vt=vt_int,st=st_int,pit1=pit1_int,yt1=yt1_int,st1=st1_int)
print(f'The equilibrium value of y, given variable values and parameters is: {y_star}')

##  Solving for pi

# a. Defining a lambda function of yt_eq
_sol_pi_func = sm.lambdify((h,b,alpha,gamma,phi,yt,vt,st,pit1,yt1,st1),pit_star)

# b. Defining a function which solve for pi, given values and parameters
def sol_pi_func(vt,st,yt,pit1,yt1,st1,h=par['h'],alpha=par['alpha'],b=par['b'],gamma=par['gamma'],phi=par['phi']):
    return _sol_pi_func(h,b,alpha,gamma,phi,yt,vt,st,pit1,yt1,st1)

# c. Solving for pi, using the value of y_star, and printing the solution
pi_star = sol_pi_func(vt=vt_int,st=st_int,yt=y_star,pit1=pit1_int,yt1=yt1_int,st1=st1_int)
print(f'The equilibrium value of pi, given variable values and parameters is: {pi_star}')

```

    The equilibrium value of y, given variable values and parameters is: 0.0
    The equilibrium value of pi, given variable values and parameters is: 0.0

```python
# Illustration

# Define AD and SRAS curves as functions
def AD_curve(h,b,alpha,vt,yt):
    return 1/(h*alpha)*(vt-(1+b*alpha)*yt)

def SRAS_curve(gamma,phi,pit1,yt,yt1,st,st1):
    return pit1+gamma*yt-phi*gamma*yt1+st-phi*st1
```

```python
## DENNE CELLE SKAL IKKE NØDVENDIGVIS BRUGES

# Constructing graph

def harry(vt_val):
    # a. Finding equilibrium given vt
    y_opt = sol_y_func(vt=vt_val,st=st_int,pit1=pit1_int,yt1=yt1_int,st1=st1_int)
    pi_opt = sol_pi_func(vt=vt_val,st=st_int,yt=y_star,pit1=pit1_int,yt1=yt1_int,st1=st1_int)

    # b. Constructing curves
    y = np.linspace(-0.1,0.1,100)
    AD = AD_curve(h=par['h'],b=par['b'],alpha=par['alpha'],vt=vt_val,yt=y)
    SRAS = SRAS_curve(gamma=par['gamma'],phi=par['phi'],pit1=pit1_int,yt1=yt1_int,st=st_int,st1=st1_int,yt=y)

    # c. Generating graph
    # i.  Font sizes
    title_font = {'size':'15'}
    label_font = {'size':'12'}
    # ii. Graph
    plt.figure(figsize=(10,6))
    plt.plot(y,AD,label='AD')
    plt.plot(y,SRAS,label='SRAS')
    plt.grid(True)
    # iii. Title and labels
    plt.title('AD and SRAS', **title_font)
    plt.xlabel('$y_t$', **label_font)
    plt.ylabel('$\pi_t$', **label_font)
    plt.ylim(-0.2,0.2)
    # c. Add lines which show optimal values of y and pi
    plt.axvline(y_opt, linestyle='dashed', color="grey")
    plt.axhline(pi_opt, linestyle='dashed', color="grey")

    plt.legend()

# Interactive graph with slider
def ron():
    widgets.interact(harry,
                     vt_val=widgets.FloatSlider(
                         value=0,
                         description='Value of $v_t$',
                         min=0,
                         max=0.1,
                         step=0.01,
                         disabled=False,
                         continuous_update=False));

# ron()

# Interactive graph with dropdown
def hermione():
    widgets.interact(harry,
                    vt_val=widgets.Dropdown(
                    description='Value of $v_t$',
                    options=[0,0.1],
                    value=0),);

# hermione();

```

```python
# Calculating equilibrium values for vt = 0 and vt=0.1
y_star_new = sol_y_func(vt=vt_one,st=st_int,pit1=pit1_int,yt1=yt1_int,st1=st1_int)
pi_star_new = sol_pi_func(vt=vt_one,st=st_int,yt=y_star,pit1=pit1_int,yt1=yt1_int,st1=st1_int)
print(f'The equilibrium value of y and pi, given vt = 0: \v ({y_star};{pi_star})')
print(f'The equilibrium value of y and pi, given vt = 0.1: \v ({y_star_new:.3};{pi_star_new:.3})')

def snape():
    # a. Finding equilibrium given vt=0 and vt=0.1
    y_one = sol_y_func(vt=0,st=st_int,pit1=pit1_int,yt1=yt1_int,st1=st1_int)
    pi_one = sol_pi_func(vt=0,st=st_int,yt=y_star,pit1=pit1_int,yt1=yt1_int,st1=st1_int)
    y_two = sol_y_func(vt=0.1,st=st_int,pit1=pit1_int,yt1=yt1_int,st1=st1_int)
    pi_two = sol_pi_func(vt=0.1,st=st_int,yt=y_star,pit1=pit1_int,yt1=yt1_int,st1=st1_int)

    # b. Constructing curves
    y = np.linspace(-0.1,0.1,100)
    AD_one = AD_curve(h=par['h'],b=par['b'],alpha=par['alpha'],vt=0,yt=y)
    SRAS_one = SRAS_curve(gamma=par['gamma'],phi=par['phi'],pit1=pit1_int,yt1=yt1_int,st=st_int,st1=st1_int,yt=y)
    AD_two = AD_curve(h=par['h'],b=par['b'],alpha=par['alpha'],vt=0.1,yt=y)
    SRAS_two = SRAS_curve(gamma=par['gamma'],phi=par['phi'],pit1=pit1_int,yt1=yt1_int,st=st_int,st1=st1_int,yt=y)

    # c. Generating graph
    # i.  Font sizes
    title_font = {'size':'15'}
    label_font = {'size':'12'}
    # ii. Graph
    plt.figure(figsize=(10,6))
    plt.plot(y,AD_one,color='orange',label='AD, $v_t$=0')
    plt.plot(y,SRAS_one,color='navy',label='SRAS, $v_t$={0;0.1}')
    plt.plot(y,AD_two,color='goldenrod',label='AD, $v_t$=0.1')
    plt.grid(True)
    # iii. Title and labels
    plt.title('AD and SRAS', **title_font)
    plt.xlabel('$y_t$', **label_font)
    plt.ylabel('$\pi_t$', **label_font)
    # c. Add lines and marks which show optimal values of y and pi
    plt.plot(y_two,pi_two, marker='o',color='goldenrod',markersize='8')
    plt.axvline(y_one, linestyle='dashed', color="grey")
    plt.axhline(pi_one, linestyle='dashed', color="grey")
    # plt.axvline(y_two, linestyle='dashed', color="lightblue")
    # plt.axhline(pi_two, linestyle='dashed', color="lightblue")

    plt.legend()

snape();
```

**Persistent disturbances:** Now, additionaly, assume that both the demand and the supply disturbances are AR(1) processes

$$ v_{t} = \delta v_{t-1} + x_{t} $$
$$ s_{t} = \omega s_{t-1} + c_{t} $$

where $x_{t}$ is a **demand shock**, and $c_t$ is a **supply shock**. The **autoregressive parameters** are:

```python
par['delta'] = 0.80
par['omega'] = 0.15
```

## Question 3:
Starting from $y_{-1} = \pi_{-1} = s_{-1} = 0$, how does the economy evolve for $x_0 = 0.1$, $x_t = 0, \forall t > 0$ and $c_t = 0, \forall t \geq 0$?

```python
# Constructing AR(1) processes

# Defining intial values
y1      = 0
pi_init = 0
s_init  = 0
v_init  = 0
xt      = 0
ct      = 0
x0      = 0.1

# Number of variable values
num_fig = 200
num = 1000

# Creating list, contraining x number values of vt
def v_func(number=num):
    """
    Function which creates list of vt values
    Args:
        number = Number of values in list
    Returns:
        List of vt values
    """
    vt = []
    vt_num = par['delta'] * v_init + x0
    vt.append(vt_num)
    for i in range(0,number):
        vt_i = par['delta']*vt[i] + xt
        vt.append(vt_i)

    return vt

vt = v_func(num_fig);

# Creating list, contraining x number values of st
def s_func(number=num):
    """
    Function which creates list of st values
    Args:
        number: Number of values in list
    Returns:
        List of st values
    """
    st = []
    st_num = par['omega']* s_init + ct
    st.append(st_num)
    for i in range(0,number):
        st_i = par['omega']*st[i] + ct
        st.append(st_i)

    return st

st = s_func(num_fig);

```

```python
# Constructing list of y* and pi*
y_stars = [0]
pi_stars = [0]

def y_solve(number=num):
    """
    Solves economy and obtains equilibrum values of y and pi in each period
    using the sol_y_func function.
    Args:
        number: Number of periods
    Returns:
        Appends equilibrum values of y to the list y_stars
        Appends qeuilibrium values of pi to the list pi_stars
    """
    for i in range(1,number):
        # y
        y_star_i = sol_y_func(vt=vt[i],st=st[i],pit1=pi_stars[i-1],yt1=y_stars[i-1],st1=st[i-1])
        y_stars.append(y_star_i)
        # Pi
        pi_star_i = sol_pi_func(vt=vt[i],st=st[i],yt=y_stars[i],pit1=pi_stars[i-1],yt1=y_stars[i-1],st1=st[i-1])
        pi_stars.append(pi_star_i)

y_solve(num_fig);

```

```python
# Illustrating the evolution of the economy
x_axis = np.linspace(0,num_fig,num_fig)

# Constructing graph
f, (ax1, ax2) = plt.subplots(1, 2, sharey=True, figsize=(10,5))
ax1.plot(x_axis,pi_stars, label='$\pi$')
ax1.set_title('Values of $\pi$')
ax1.grid(True)
ax1.legend()
ax2.plot(x_axis,y_stars, label="$y$")
ax2.set_title('Values of y')
ax2.grid(True)
ax2.legend()

plt.show();

```

```python
# Illustrating the evolution of the economy
plt.plot(y_stars,pi_stars)
plt.scatter(y_stars,pi_stars)
plt.grid(True)

plt.title("Evolution of the Economy with AR(1) processes")
plt.xlabel('$y_t$')
plt.ylabel('$\pi_t$')
plt.text(0.006,0.0045,'Each dot is the equilibrium in a period', bbox=dict(facecolor='white', alpha=0.5))

plt.show();

```

```python
# Illustrating moves in AD and SRAS curves

# Construcnting v, s and y vectors with 1000 values
vt = v_func();
st = s_func();
y_stars = [0]
y_solve();

# a. Shocks and Convergence in AD and SRAS curves
def AD_shock(yt,period, h=par['h'],b=par['b'],alpha=par['alpha'],vt=vt):
    """Function which generates AD curves following a shock
    Args:
        yt: Vector of y values
        period: Number of periods after initial shock
        h: Parameter
        b: Parameter
        alpha: Parameter
        vt: Vector of shocks
    Returns:
        AD curve with shocks.
    """
    if period == 0: # Assuming zero shocks in initial period
        return 1/(h*alpha)*(0-(1+b*alpha)*yt)
    else:
        return 1/(h*alpha)*(vt[period-1]-(1+b*alpha)*yt)

def SRAS_shock(yt,period,gamma=par['gamma'],phi=par['phi'],st=st):
    """Function which generates SRAS curves following a shock
    Args:
        yt: Vector of y values
        period: Number of periods after initial shock
        gamma: Parameter
        phi: Parameter
        pi: Vector of pi values
        yt: Vector of y values
        st: Vector of shocks
    Returns: SRAS curve with shocks
        """
    if period == 0: # Assuming zero shocks in initial period
        return pi_init + gamma*yt - phi*y1 + st[period] - phi*s_init
    else:
        y_values = [0]
        pi_values = [0]
        for i in range(1,num):
            y_val_i = sol_y_func(vt=vt[i],st=st[i],pit1=pi_stars[i-1],yt1=y_stars[i-1],st1=st[i-1])
            y_values.append(y_val_i)
            pi_val_i = sol_pi_func(vt=vt[i],st=st[i],yt=y_stars[i],pit1=pi_stars[i-1],yt1=y_stars[i-1],st1=st[i-1])
            pi_values.append(pi_val_i)
        return pi_values[period-1] + gamma*yt - phi*gamma*y_values[period-1] + st[period] - phi*st[period-1]

# b. Constructing graph

def sherlock(period):
    """Constructs figure of convergence in the economy following a shock
    Args:
        period: Number of periods after inital shock
    Returns:
        Graph of AD and SRAS curves x periods after a shock
    Note:
        The graph is meant to be called through the watson function, which turn the graph interactive.
        Thus, the function is not intented to be called on its own
    """
    # i. Constructing curves
    y = np.linspace(-0.05,0.05,100)
    AD_zero = AD_shock(y,0,vt=vt)
    AD_i = AD_shock(y,period,vt=vt)

    SRAS_zero = SRAS_shock(y,0,st=st)
    SRAS_i = SRAS_shock(y,period,st=st)

    # ii. Graphs
    # (a) Font sizes
    title_font = {'size':'20'}
    label_font = {'size':'18'}
    # (b) AD curves
    plt.figure(figsize=(12,6))
    plt.plot(y,AD_zero,label=('AD'),linewidth=3,color='orange')
    plt.plot(y,AD_i,label='$AD new$',color='goldenrod')

    # (c) SRAS curves
    plt.plot(y,SRAS_zero,label='SRAS',linewidth=3,color='navy')
    plt.plot(y,SRAS_i,label='$SRAS new$',color='purple')

    # (d) Graph properties
    plt.grid(True)
    plt.title('AD and SRAS', **title_font)
    plt.xlabel('$y_t$', **label_font)
    plt.ylabel('$\pi_t$', **label_font)
    plt.ylim(-0.1,0.12)

    # d. Add lines which show optimal values of y and pi
    plt.axvline(y_star, linestyle='dashed', label='$y^*$')
    plt.axhline(pi_star, linestyle='dashed', label='$\pi^*$')

    plt.legend()

# Turn graph interactive
def watson():
    widgets.interact(sherlock,
                    period=widgets.Dropdown(
                    description='Period after initial shock',
                    options=[1,5,10,50,100,500,1000],
                    value=1),
                    );
# watson();

def lestrade():
    widgets.interact(sherlock,
                     period=widgets.IntSlider(
                         value=0,
                         description='Period',
                         min=0,
                         max=100,
                         step=1,
                         disabled=False,
                         continuous_update=False));

lestrade();
```

    interactive(children=(IntSlider(value=0, continuous_update=False, description='Period'), Output()), _dom_class…

**Stochastic shocks:** Now, additionally, assume that $x_t$ and $c_t$ are stochastic and normally distributed

$$ x_{t}\sim\mathcal{N}(0,\sigma_{x}^{2}) $$
$$ c_{t}\sim\mathcal{N}(0,\sigma_{c}^{2}) $$

The **standard deviations of the shocks** are:

```python
par['sigma_x'] = 3.492
par['sigma_c'] = 0.2
```

## Question 4
Simulate the AS-AD model for 1,000 periods. Calculate the following five statistics:

1. Variance of $y_t$, $var(y_t)$
2. Variance of $\pi_t$, $var(\pi_t)$
3. Correlation between $y_t$ and $\pi_t$, $corr(y_t,\pi_t)$
4. Auto-correlation between $y_t$ and $y_{t-1}$, $corr(y_t,y_{t-1})$
5. Auto-correlation between $\pi_t$ and $\pi_{t-1}$, $corr(\pi_t,\pi_{t-1})$

```python
# Constructing x and c
seed = 11
np.random.seed(seed)

x = np.random.normal(0,par['sigma_x'],1000)
x[0] = 0
c = np.random.normal(0,par['sigma_c'],1000)
c[0] = 0
```

```python
# New vt and st vector

# a. Number of variable values
num = 1000

# b. Creating list, contraining x number values of vt
vt_new = [0]
def v_func(number=num):
    """
    Function which creates list of vt values
    Args:
        number = Number of values in list
    Returns:
        List of vt values when vt is stochastic
    """
    for i in range(1,number):
        vt_i = par['delta']*vt_new[i-1] + x[i]
        vt_new.append(vt_i)

    return vt_new

vt_new = v_func();

# c. Creating list, contraining x number values of st
def s_func(number=num):
    """
    Function which creates list of st values
    Args:
        number = Number of values in list
    Returns:
        List of st values when st is stochastic
    """
    st_new = [0]
    for i in range(1,number):
        st_i = par['omega']*st_new[i-1] + c[i]
        st_new.append(st_i)

    return st_new

st_new = s_func();
```

```python
# Illustrating AD and SRAS curves with new shocks
# b. Constructing graph

def mycraft(period):
    """Constructs figure of convergence in the economy following a shock
    Args:
        period: Number of periods after inital shock
    Returns:
        Graph of AD and SRAS curves with stochastic shocks
    Note:
        The graph is meant to be called through the irene function, which turn the graph interactive.
        Thus, the function should not be called on its own.
    """
    # i. Constructing curves
    y = np.linspace(-1,1,100)
    AD_zero = AD_shock(y,0,vt=vt_new)
    AD_i = AD_shock(y,period,vt=vt_new)

    SRAS_zero = SRAS_shock(y,0,st=st_new)
    SRAS_i = SRAS_shock(y,period,st=st_new)

    # ii. Graphs
    # (a) Font sizes
    title_font = {'size':'20'}
    label_font = {'size':'15'}
    # (b) AD curves
    plt.figure(figsize=(12,6))
    plt.plot(y,AD_zero,label=('AD'),linewidth=3,color='orange')
    plt.plot(y,AD_i,label='$AD new$',color='goldenrod')

    # (c) SRAS curves
    plt.plot(y,SRAS_zero,label='SRAS',linewidth=3,color='navy')
    plt.plot(y,SRAS_i,label='$SRAS new $',color='purple')

    # (d) Graph properties
    plt.grid(True)
    plt.title('AD and SRAS', **title_font)
    plt.ylim(-5,5)
    plt.xlabel('$y_t$', **label_font)
    plt.ylabel('$\pi_t$', **label_font)

    # d. Add lines which show optimal values of y and pi
    plt.axvline(y_star, linestyle='dashed', label='$y^*$')
    plt.axhline(pi_star, linestyle='dashed', label='$\pi^*$')

    plt.legend(loc='upper right')

# Turn graph interactive
def irene():
    widgets.interact(mycraft,
                    period=widgets.Dropdown(
                    description='Period after initial shock',
                    options=[1,5,10,50,100,200,600,800],
                    value=1),
                    );

#irene();

def moriarty():
    widgets.interact(mycraft,
                     period=widgets.IntSlider(
                         value=0,
                         description='Period',
                         min=0,
                         max=999,
                         step=10,
                         disabled=False,
                         continuous_update=False));

moriarty();
```

    interactive(children=(IntSlider(value=0, continuous_update=False, description='Period', max=999, step=10), Out…

# Tilføj Equilibrium Path

```python
# Simulating the Economy

# Adding equilibrum values as initial values
y_sim = [0]
pi_sim = [0]

for i in range(1,num):
    y_val_i = sol_y_func(vt=vt_new[i],st=st_new[i],pit1=pi_sim[i-1],yt1=y_sim[i-1],st1=st_new[i-1])
    y_sim.append(y_val_i)
    pi_val_i = sol_pi_func(vt=vt_new[i],st=st_new[i],yt=y_sim[i],pit1=pi_sim[i-1],yt1=y_sim[i-1],st1=st_new[i-1])
    pi_sim.append(pi_val_i)
```

```python
# Calculate variances of y_sim and pi_sim
y_var = np.var(y_sim)
pi_var = np.var(pi_sim)

# Calculate correlation between y_sim and pi_sim
y_pi_matrix = np.corrcoef(y_sim,pi_sim)
y_pi_corr = y_pi_matrix[0,1]

# Calculate autocorrelations
y_matrix = np.corrcoef(y_sim[:-1],y_sim[1:])
y_corr = y_matrix[0,1]
pi_matrix = np.corrcoef(pi_sim[:-1],pi_sim[1:])
pi_corr = pi_matrix[0,1]

print(f'The variance of y is: {y_var:22.4f}')
print(f'The variance of pi is: {pi_var:21.4f}')
print(f'The correlation between y and pi is: {y_pi_corr:.4f}')
print(f'The autocorrelation of y is: {y_corr:15.4f}')
print(f'The autocorrelation of pi is: {pi_corr:14.4f}')
```

    The variance of y is:                 1.8036
    The variance of pi is:                1.2007
    The correlation between y and pi is: -0.0826
    The autocorrelation of y is:          0.7575
    The autocorrelation of pi is:         0.9797

## Question 5
Plot how the correlation between $y_t$ and $\pi_t$ changes with $\phi$. Use a numerical optimizer or root finder to choose $\phi\in(0,1)$ such that the simulated correlation between $y_t$ and $\pi_t$ comes close to 0.31.

```python
# Defining the correlation as a function

def superman(s):
    y_phi = np.zeros(num)
    pi_phi = np.zeros(num)
    for i in range(1,num):
        y_sim_new = sol_y_func(vt=vt_new[i],st=st_new[i],pit1=pi_phi[i-1],yt1=y_phi[i-1],st1=st_new[i-1],phi=s)
        y_phi[i] = y_sim_new
        pi_sim_new = sol_pi_func(vt=vt_new[i],st=st_new[i],yt=y_phi[i],pit1=pi_phi[i-1],yt1=y_phi[i-1],st1=st_new[i-1],phi=s)
        pi_phi[i] = pi_sim_new
    return np.corrcoef(y_phi,pi_phi)[0,1]

# Finding the value of phi which give the correlation 0.31
x0 = 0
def batman(x):
    return (superman(x)-0.31)**2

obj_func = lambda x: superman(x)-0.31

result = optimize.minimize(batman,x0)
phi_val = result.x[0]
print(f'The value of phi that makes corr(y,pi) = 0.31 is phi = {phi_val:.3f}')

# Plot
x = np.linspace(0,1,100)
y = np.empty(100)
for i,X in enumerate(x):
    y[i] = superman(X)

# i.  Font sizes
title_font = {'size':'15'}
label_font = {'size':'12'}
# ii. Figure
plt.figure(figsize=(10,6))
plt.plot(x,y)
plt.plot(phi_val,0.31, marker='o')
plt.axvline(phi_val, linestyle='dotted', color="orange", label=f'$\phi$ = {phi_val:.3f}')
plt.axhline(0.31, linestyle='dashed', color="orange", label='corr(y,$\pi$) = 0.31')
plt.title('Correlation between $y_t$ and $\pi_t$', **title_font)
plt.xlabel('$\phi$', **label_font)
plt.ylabel('Correlation', **label_font)
plt.grid(True)
plt.legend()

plt.show();

```

## Quesiton 6
Use a numerical optimizer to choose $\sigma_x>0$, $\sigma_c>0$ and $\phi\in(0,1)$ to make the simulated statistics as close as possible to US business cycle data where:

1. $var(y_t) = 1.64$
2. $var(\pi_t) = 0.21$
3. $corr(y_t,\pi_t) = 0.31$
4. $corr(y_t,y_{t-1}) = 0.84$
5. $corr(\pi_t,\pi_{t-1}) = 0.48$

```python
# Generating function to make the demand shock vector
def v_func(num,pars):
    """
    Function which creates list of vt values
    Args:
        number = Number of values in list
    Returns:
        Array of vt values
    """
    vt_sim = np.zeros(num)
    x = np.random.normal(0,pars[0],num)

    for i in range(1,num):
        vt_i = par['delta']*vt_sim[i-1] + x[i]
        vt_sim[i] += vt_i

    return vt_sim

# Generating function to make the supply chok vector
def s_func(num,pars):
    """
    Function which creates list of st values
    Args:
        number = Number of values in list
    Returns:
        Array of st values
    """

    st_sim = np.zeros(num)
    c = np.random.normal(0,pars[1],num)

    for i in range(1,num):
        st_i = par['omega']*st_sim[i-1] + c[i]
        st_sim[i] += st_i

    return st_sim
```

```python
# Defining function that returns the vectors of the equilibruim y and pi
def superman(num, pars):
    """
    Simulates y and pi values given shocks
    Args:
        num: Number of values in each array
        pars: Dictionary of parameter values
    Returns:
        Array of simulated y values, y_sim
        Array of simulated pi values pi_sim
    """
    y_sim = np.zeros(num)
    pi_sim = np.zeros(num)

    vt_sim = v_func(num,pars)
    st_sim = s_func(num,pars)

    for i in range(1,num):
        y_val_i = sol_y_func(vt=vt_sim[i],st=st_sim[i],pit1=pi_sim[i-1],yt1=y_sim[i-1],st1=st_sim[i-1],phi=pars[2])
        y_sim[i] += y_val_i
        pi_val_i = sol_pi_func(vt=vt_sim[i],st=st_sim[i],yt=y_sim[i],pit1=pi_sim[i-1],yt1=y_sim[i-1],st1=st_sim[i-1],phi=pars[2])
        pi_sim[i] += pi_val_i

    return y_sim, pi_sim

# Defining function that returns the variance, correlation, and autocorrelation of y and pi
def batman(num, pars): # ar_corr_acorr
    """
    Estimates variance of y, variance of pi, correlation between y and pi
    autocorrelation of y and autocorrelation of pi
    Arg:
        num: Number of values in each y and pi array
        pars: Dictionary of parameter values
    Returns:
        Variance of y, var_y
        Variance of pi, var_pi
        Correlation between y and pi, corr_ypi
        Autocorrelaiton of y, acorr_y
        Autocorrelation of pi, acorr_pi
    """
    y_sim, pi_sim = superman(num, pars)

    var_y = np.var(y_sim)
    var_pi = np.var(pi_sim)
    corr_ypi = np.corrcoef(y_sim,pi_sim)[0,1]
    acorr_y = np.corrcoef(y_sim[:-1],y_sim[1:])[0,1]
    acorr_pi = np.corrcoef(pi_sim[:-1],pi_sim[1:])[0,1]

    return var_y, var_pi, corr_ypi, acorr_y, acorr_pi

```

```python
# Define function which finds the parameters of sigma_x, sigma_c and phi that minimize the difference
# between moments of the model with the moments of the US economy
def wolverine(num):
    """
    Function finds values of sigma_x, sigma_c and phi which minimize the diffence between the
    moments of the model and the moments of the US economy
    Args:
        num: Number of simulations
    Returns:
        Results from optimization process
    """

    def _flash(num,pars):
        seed = 11
        np.random.seed(seed)

        var_y, var_pi, corr_ypi, acorr_y, acorr_pi = batman(num, pars)

        test = np.zeros(5)

        test[0] += (var_y-1.64)**2
        test[1] += (var_pi-0.21)**2
        test[2] += (corr_ypi-0.31)**2
        test[3] += (acorr_y-0.84)**2
        test[4] += (acorr_pi-0.48)**2

        return  np.sum(test)

    flash= lambda x: _flash(num,x)

    #print(flash([par['sigma_x'], par['sigma_c'],par['phi']]))

    x0 = [par['sigma_x'], par['sigma_c'],phi_val]
    bounds = ((0.0001,10000000), (0.0001,10000000), (0,1))

    result = optimize.minimize(flash,x0,
                           method='SLSQP',
                           bounds=bounds
                           )

    return result

result = wolverine(num)
var_y, var_pi, corr_ypi, acorr_y, acorr_pi = batman(num, result.x)
#print(f'{result}\n \n')

text = f'The optimal values of the parameters is:\n'
text += f'sigma_x = {result.x[0]:.3f}\n'
text += f'sigma_c = {result.x[1]:.3f}\n'
text += f'phi     = {result.x[2]:.3f}\n \n'
print(text)

text = f'The moment of the model and the US economy is: \n'
text += f'             The model  US economy \n'
text += f'var(y):         {var_y:.2f}       {1.64}\n'
text += f'var(pi):        {var_pi:.2f}       {0.21}\n'
text += f'corr(y,pi):     {corr_ypi:.2f}       {0.31}\n'
text += f'corr(y,y_1):    {acorr_y:.2f}       {0.84}\n'
text += f'corr(pi,pi_1):  {acorr_pi:.2f}       {0.48}\n'

print(text)
```

    The optimal values of the parameters is:
    sigma_x = 3.124
    sigma_c = 0.219
    phi     = 0.951

    The moment of the model and the US economy is:
                 The model  US economy
    var(y):         1.57       1.64
    var(pi):        0.06       0.21
    corr(y,pi):     0.33       0.31
    corr(y,y_1):    0.79       0.84
    corr(pi,pi_1):  0.41       0.48

```python

```

```python

```


# Algorithms Exercise 2

## Imports

```python
%matplotlib inline
from matplotlib import pyplot as plt
import seaborn as sns
import numpy as np
```

## Peak finding

Write a function `find_peaks` that finds and returns the indices of the local maxima in a sequence. Your function should:

* Properly handle local maxima at the endpoints of the input array.
* Return a Numpy array of integer indices.
* Handle any Python iterable as input.

```python
def find_peaks(a):
    """Find the indices of the local maxima in a sequence."""
    leest = []
    if a[0] > a[1]:
        leest.append(0)
    for x in range(1,len(a)-1):
        if (a[x-1]<a[x]) and (a[x]>a[x+1]):
            leest.append(x)
    if a[len(a)-1] > a[len(a)-2]:
        leest.append(len(a)-1)
    return leest

```

```python
p1 = find_peaks([2,0,1,0,2,0,1])
assert np.allclose(p1, np.array([0,2,4,6]))
p2 = find_peaks(np.array([0,1,2,3]))
assert np.allclose(p2, np.array([3]))
p3 = find_peaks([3,2,1,0])
assert np.allclose(p3, np.array([0]))
```

Here is a string with the first 10000 digits of $\pi$ (after the decimal). Write code to perform the following:

* Convert that string to a Numpy array of integers.
* Find the indices of the local maxima in the digits of $\pi$.
* Use `np.diff` to find the distances between consequtive local maxima.
* Visualize that distribution using an appropriately customized histogram.

```python
from sympy import pi, N
pi_digits_str = str(N(pi, 10001))[2:]
```

```python
x = np.zeros(10000, dtype=int )
for a in range(len(pi_digits_str)):
    x[a] = int(pi_digits_str[a])

ind = find_peaks(x)
dis = np.diff(ind)

plt.hist(dis, bins=20, range=(0,15))
plt.title('Distribution of local Maxima in Pi')
plt.xlabel('Distantce between local maxima')
plt.ylabel('Frequency')
plt.show()

```

```python
assert True # use this for grading the pi digits histogram
```


# Trading Lists and Trading Trajectories

### Introduction

[Almgren and Chriss](https://cims.nyu.edu/~almgren/papers/optliq.pdf) provided a solution to the optimal liquidation problem by assuming the that stock prices follow a discrete arithmetic random walk, and that the permanent and temporary market impact functions are linear functions of the trading rate.

Almgren and Chriss showed that for each value of risk aversion there is a unique optimal execution strategy. This optimal execution strategy is determined by a trading trajectory and its associated trading list. The optimal trading trajectory is given by:

\begin{equation}
x_j = \frac{\sinh \left( \kappa \left( T-t_j\right)\right)}{ \sinh (\kappa T)}X, \hspace{1cm}\text{ for } j=0,...,N
\end{equation}

and the associated trading list is given by:

\begin{equation}
n_j = \frac{2 \sinh \left(\frac{1}{2} \kappa \tau \right)}{ \sinh \left(\kappa T\right) } \cosh \left(\kappa \left(T - t_{j-\frac{1}{2}}\right)\right) X, \hspace{1cm}\text{ for } j=1,...,N
\end{equation}

where $t_{j-1/2} = (j-\frac{1}{2}) \tau$.

Given some initial parameters, such as the number of shares, the liquidation time, the trader's risk aversion, etc..., the trading list will tell us how many shares we should sell at each trade to minimize our transaction costs.

In this notebook, we will see how the trading list varies according to some initial trading parameters.

## Visualizing Trading Lists and Trading Trajectories

Let's assume we have 1,000,000 shares that we wish to liquidate. In the code below, we will plot the optimal trading trajectory and its associated trading list for different trading parameters, such as trader's risk aversion, number of trades, and liquidation time.

```python
%matplotlib inline

import matplotlib.pyplot as plt

# Add-on : Hide Matplotlib deprecate warnings
import warnings
warnings.filterwarnings("ignore")
# High resolution plot outputs for retina display
%config InlineBackend.figure_format = 'retina'

import utils

# We set the default figure size
plt.rcParams['figure.figsize'] = [17.0, 7.0]

# Set the number of days to sell all shares (i.e. the liquidation time)
l_time = 60

# Set the number of trades
n_trades = 60

# Set the trader's risk aversion
t_risk = 1e-6

# Plot the trading list and trading trajectory. If show_trl = True, the data frame containing the values of the
# trading list and trading trajectory is printed
utils.plot_trade_list(lq_time = l_time, nm_trades = n_trades, tr_risk = t_risk, show_trl = True)
```

# Implementing a Trading List

Once we have the trading list for a given set of initial parameters, we can actually implement it. That is, we can sell our shares in the stock market according to the trading list and see how much money we made or lost. To do this, we are going to simulate the stock market with a simple trading environment. This simulated trading environment uses the same price dynamics and market impact functions as the Almgren and Chriss model. That is, stock price movements evolve according to a discrete arithmetic random walk and the permanent and temporary market impact functions are linear functions of the trading rate. We are going to use the same environment to train our Deep Reinforcement Learning algorithm later on.

We will describe the details of the trading environment in another notebook, for now we will just take a look at its default parameters. We will distinguish between financial parameters, such the annual volatility in stock price, and the parameters needed to calculate the trade list using the Almgren and Criss model, such as the trader's risk aversion.

```python
import utils

# Get the default financial and AC Model parameters
financial_params, ac_params = utils.get_env_param()
```

### Default Financial Parameters

```python
financial_params
```

Financial Parameters

  Annual Volatility:	  12%	   Bid-Ask Spread:    	     0.125

  Daily Volatility:	  0.8%	   Daily Trading Volume:	 5,000,000

### Parameters for the Almgren and Chriss Model

```python
ac_params
```

Almgren and Chriss Model Parameters

  Total Number of Shares to Sell:	                  1,000,000	   Fixed Cost of Selling per Share:	 $0.062

  Starting Price per Share:	                         $50.00	     Trader's Risk Aversion:	           1e-06

  Price Impact for Each 1% of Daily Volume Traded:	 $2.5e-06	    Permanent Impact Constant:	       2.5e-07

  Number of Days to Sell All the Shares:	              60	       Single Step Variance:	             0.144

  Number of Trades:	                                   60	       Time Interval between trades:	      1.0

The code below implements the trading list resulting from different trading parameters, such as trader's risk aversion, number of trades, and liquidation time. All other parameters, such as total number shares to sell, are taken from the simulated trading environment (see above).

```python
%matplotlib inline

import matplotlib.pyplot as plt

import utils

# We set the default figure size
plt.rcParams['figure.figsize'] = [17.0, 7.0]

# Set the random seed
sd = 0

# Set the number of days to sell all shares (i.e. the liquidation time)
l_time = 60

# Set the number of trades
n_trades = 60

# Set the trader's risk aversion
t_risk = 1e-6

# Implement the trading list for the given parameters
utils.implement_trade_list(seed = sd, lq_time = l_time, nm_trades = n_trades, tr_risk = t_risk)
```

```python

```


```python
%pylab inline
%config InlineBackend.figure_format = 'retina'
from ipywidgets import interact
import scipy.stats as stats
import pandas as pd
# import pymc3 as pm
import arviz as az
import seaborn as sns

import tensorflow as tf
import tensorflow_probability as tfp
tfd = tfp.distributions
tfb = tfp.bijectors

import warnings # the warning spam is pointless and annoying
warnings.simplefilter(action="ignore", category=FutureWarning)
```

    Populating the interactive namespace from numpy and matplotlib

    /anaconda3/lib/python3.7/site-packages/pandas/compat/_optional.py:138: UserWarning: Pandas requires version '2.7.0' or newer of 'numexpr' (version '2.6.9' currently installed).
      warnings.warn(msg, UserWarning)
    /anaconda3/lib/python3.7/site-packages/dask/config.py:168: YAMLLoadWarning: calling yaml.load() without Loader=... is deprecated, as the default Loader is unsafe. Please read https://msg.pyyaml.org/load for full details.
      data = yaml.load(f.read()) or {}
    /anaconda3/lib/python3.7/site-packages/distributed/config.py:20: YAMLLoadWarning: calling yaml.load() without Loader=... is deprecated, as the default Loader is unsafe. Please read https://msg.pyyaml.org/load for full details.
      defaults = yaml.load(f)

# Our idea from last week was to add "momentum" to our Markov chain

\begin{align*}
 \dot{x}(t) & = p(t)\\
 \dot{p}(t) &= \nabla_x L(x(t)).
\end{align*}

# Definition: Phase Space
We call the pair $x, p \in \mathbb{R}^d \times\mathbb{R}^d$ the phase space.

# Hamiltonian dynamical system has a constant of motion
One can show that the "energy"
$$\mathcal{H}(x, p) = -L(x) + \frac{1}{2}p^T M(x) p,$$
is constant (wrt time) along solutions to the Hamiltonian ODEs. That is, if $x(t)$ and $p(t)$ are solutions to the ODE, then
$$\mathcal{H}(x(t), p(t)) = -L(x(t)) + \frac{1}{2}p(t)^T M(x(t)) p(t) = \text{constant wrt }t $$

# Test problem

```python
# A = rand(2, 2)
A = array([
    [0.8, 0.05],
    [0.7, 0.5]])
# A = eye(2)

M = A.T@A
def L(_x):
    xT = _x[..., None, :]
    x = _x[..., None]
    return -0.5*(xT@M@x)[..., 0, 0]

def gradL(x):
    return -(A@x[..., None])[..., 0]

def H(x, p):
    return -L(x) + 0.5*(p**2).sum(axis=-1)

x = linspace(-5, 5, 200)
y = x
X, Y = meshgrid(x, y)
Z = L(stack([X, Y], 2))

pcolor(X, Y, exp(Z))
```

# Leap-Frog integrator
\begin{align}
p(t + 0.5\Delta t) &= p(t) + 0.5\Delta t \nabla L(x(t)) \\
x(t + \Delta t) &= x(t) + \Delta t p(t + 0.5\Delta t) \\
p(t + \Delta t) &= p(t + 0.5\Delta t)  + 0.5\Delta t \nabla L (x(t + \Delta t))
\end{align}
Alternatively, let $x_k = x(k\Delta t)$ and $p_k = p(k\Delta t)$. Then, we can write the method as an iterative formula.

For $k=0,1,2, \ldots, K$:
\begin{align}
\hat{p} &= p_k + 0.5\Delta t \nabla L(x_k) \\
x_{k+1} &= x_k + \Delta t \hat{p} \\
p_{k+1} &= \hat{p}  + 0.5\Delta t \nabla L (x_{k+1}).
\end{align}

```python
K = 200
dt = 0.1

Xsol = zeros([K, 2])
Psol = zeros([K, 2])

Xsol[0, :] = [2., 2.]
Psol[0, :] = normal(0, 1, [2])

for k in arange(K-1):
    ## Euler's method (nor leap frog)
    #Xsol[k+1, :] = Xsol[k, :] + dt*Psol[k, :]
    #Psol[k+1, :] = Psol[k, :] + dt*gradL(Xsol[k, :])
    ## Leap frog
    phat = Psol[k, :] + 0.5*dt*gradL(Xsol[k, :])
    Xsol[k+1, :] = Xsol[k, :] + dt*phat
    Psol[k+1, :] = phat + 0.5*dt*gradL(Xsol[k+1, :])

print('total time:', K*dt)

fig = figure(1, [15, 8])

fig.add_subplot(121)
pcolor(X, Y, exp(Z))
plot(Xsol[:, 0], Xsol[:, 1], '-r.')
xlabel(r'$x$', fontsize=24)
ylabel(r'$y$', fontsize=24);

fig.add_subplot(122)
plot(H(Xsol, Psol))
xlabel(r'$t$', fontsize=24)
ylabel(r'$\mathcal{H}$', fontsize=24);
```

```python
@interact(k=(100, K, 100))
def plot_fn(k=100):
    fig = figure(1, [12, 6])
#     pcolor(X, Y, exp(Z))
    fig.add_subplot(121)
    plot(Xsol[:k, 0], Xsol[:k, 1], 'r')
    xlabel(r'$x$', fontsize=24)
    ylabel(r'$y$', fontsize=24);
    fig.add_subplot(122)
    plot(Psol[:k, 0], Psol[:k, 1], 'r')
    xlabel(r'$p_1$', fontsize=24)
    ylabel(r'$p_2$', fontsize=24);
```

```python

```

# Volume and density are preserved along trajectories of the Hamiltonian dynamics
## Liouville's Theorem:
Let $\mathcal{H}(x, p)$ be smooth and continuous (i.e., $C^2(\mathbb{R}^d \times \mathbb{R}^d)$), and let it be a convex function of the variable $p\in \mathbb{R}^d$. Let $\rho(x, p| t)$ be a probability density function on the phase space. **The density function can be any valid function of $x$ and $p$ that satisfies the requirements of a probability density.** Then, along trajectories of the system,
\begin{align*}
 \dot{x}(t) & = \nabla_p \mathcal{H}(x(t), p(t))\\
 \dot{p}(t) &= - \nabla_x \mathcal{H}(x(t), p(t)),
\end{align*}
the density function is constant; that is,
$$ L_t \rho(x(t), p(t) | t)
= \frac{\partial \rho}{\partial t} +
\sum_{i=1}^d\left(\frac{\partial\rho}{\partial x_i}\dot{x}_i + \frac{\partial\rho}{\partial p_i}\dot{p}_i\right) = 0,$$
where $L_t$ denotes the total (or orbital) derivative with respect to time $t$. **(It's just the chain rule).**

```python

```

# Time reversibility

Hamiltonian dynamics has the remarkable property that under the reflection $p \to -p$, the dynamics runs in backward time. It is like instantly playing a GIF in reverse. The motion of the trajectory retraces its path perfectly back to its original state (with reflected momentum $-p_0$). That is,
$$p0, x0 \to p_{\tau}, x_{\tau}, \quad  -p_{\tau}, x_{\tau} \to -p0, x0 $$

$$x_{\tau}, p_{\tau} = \varphi_{\tau}((x_0, p_0))$$
$$x_{0}, -p_{0} = \varphi_{\tau}((x_{\tau}, -p_{\tau}))$$

## Example: harmonic oscillator
Let
$$L(x) = -\frac{x^2}{2}$$
so that we have the Hamiltonian dynamics given by
$$ \dot{x}  = p, \quad \dot{p} = -x ,$$
with initial conditions $x(0) = x_0$ and $p(0) = p_0$. This is equivalent to having a spring attached to a mass sliding on a frictionless surface. It is also an approximation of the motion of a pendulum.

The exact solution is given by
$$x(t) = x_0\cos(t) + p_0\sin(t), \quad p(t) = -x_0\sin(t) + p_0\cos(t).$$
We can verify this by substituting the solution into the equation, which we leave as an excersize.

Notice that, due to the even and odd symmetry of `cos()` and `sin()`, we have
$$x(-t) = x_0\cos(-t) + p_0\sin(-t) = x_0\cos(t) - p_0\sin(t).$$
Hence, the backward time solution is given by $p_0 \to -p_0$.

```python

```

```python

```

```python

```

## A continuous time Hamiltonian velocity-jump process
We can prove under reasonable assumptions that the Hamiltonian Markov chain, generalized to the case of random times $\tau \sim \text{Exponential}(\lambda)$, has a stationary density given by the *canonical distribution*,
$$ \pi(x, p) = \pi(x)\pi(p | x) \propto \exp( L(x))\exp( - \frac{1}{2}p^T M(x) p) = \exp( L(x) - \frac{1}{2}p^T M(x) p) = e^{-\mathcal{H}(x, p)},$$
where
$$ \mathcal{H}(x, p) = -L(x) + \frac{1}{2}p^T M(x) p .$$

The Hamiltonian velocity jump can be described as a deterministic path of the equation $(\dot{x}, \dot{p}) = F(x, p)$, where
$$F(x, p) =
\begin{bmatrix}
 \nabla_p \mathcal{H}(x, p) \\
 -\nabla_x \mathcal{H}(x, p)
\end{bmatrix} =
\begin{bmatrix}
 Mp \\
 \nabla_x L(x)
\end{bmatrix},
$$
interupted at random time intervals $\tau$ by a random jump in the momentum $p$. In between these random jumps at random times, the dynamics are deterministic. Hence, all stochasticity or "noise" comes from the random jumps in momentum.

## All Markov processes satisfy a Chapman-Kolmogorov equation
Let $u(x, p, t | x_0, t_0)$ be the probability density for the process $X(t), P(t)$ such that $X(t_0) = x_0$ and $P(0) \sim \text{Normal}(0, \sqrt{M^{-1}(x_0)})$. One can show that the density $u$ satisfies the integro-partial differential equation$^{1}$,
$$ \frac{\partial }{\partial t}u(x, p, t) = -\nabla_{x,p} \cdot (F(x, p) u(x, p, t)) + \lambda \int dx'\int dp' \left[W(x, p | x', p')u(x', p', t) - W(x', p' | x, p)u(x, p, t) \right], $$
with the initial condition
$$ u(x, p, t_0) = \delta(x - x_0)\text{Gaussian}(p | \mu=0, \text{cov}=M^{-1}(x_0)).$$
The density $W()$ is called the jump transition density. Given a jump occurs at time $t$, the probability density for the state to jump from $x',p'$ to $x, p$ is given by $W(x, p | x', p')$.

In our case, the jump transitions (resetting the momentum at the begining of each step) is given by $W(x, p, | x', p') = \delta(x - x')W(p | x'),$ where $W(p | x) = \text{Gaussian}(p | \mu=0, \text{cov}=M^{-1}(x))$.

The Chapman-Kolmogorov equation becomes
$$\frac{\partial }{\partial t}u(x, p, t) = -\nabla_{x,p} \cdot(F(x, p) u(x, p, t)) + \lambda  \left[W(p | x)\int dp'u(x, p', t) - u(x, p, t) \right] .$$

*1. One place to find a rigorous treatment of the differential form of the Chapman-Kolmogorov equation is Freidlin and Wentzell 2012 (https://books.google.com/books?hl=en&lr=&id=p8LFMILAiMEC&oi=fnd&pg=PR6&ots=-BHn--qj3x&sig=Acmz6q3DhxEShDFk44Bbr0G1g0o)*

## Stationary Chapman-Kolmogorov equation
We want to pick the details of the Hamiltonian velocity jump transitions so that the above is the stationary density of the process. We will assume ergodicity and uniqueness of the solution to the stationary Chapman-Kolmogorov equation, which is given by
$$ 0= -\nabla_{x,p} \cdot(F(x, p) \pi(x, p)) + \lambda \left[W(p | x)\int dp'\pi(x, p') - \pi(x, p) \right] .$$
Note that convergence to the given stationary distribution should be independent of the jump rate $\lambda > 0$.

Let $\pi(x, p) = \pi(p | x)\pi(x)$. The stationary CK equation becomes,
$$ 0 = -\nabla_{x,p} \cdot(F(x, p) \pi(p | x)\pi(x)) + \lambda  \left[W(p | x)\pi(x) - \pi(p | x)\pi(x) \right] $$
$$ = -\nabla_{x,p} \cdot(F(x, p)\pi(p | x)\pi(x)) + \lambda \pi(x) \left[W(p | x) - \pi(p | x) \right] .$$

If we set $W(p | x) = \pi(p | x)$ (a detailed balance requirement) then the last term on the right hand side vanishes and we are left with
$$ 0 = -\nabla_{x,p} \cdot(F(x, p)\pi(p | x)\pi(x)) $$
We have two approaches to show the remaining term vanishes

## Generalized detailed balance conditions guarantee a stationary distribution
Beyond the definition of detailed balance shown in a previous cell, one can go further and derive conditions for specific types of Markov processes [see Gardner: Handbook of Stochastic Methods, 3rd edition, pg 151].

For our case, detailed balance is satisfied if
$$ \begin{bmatrix}
I & \\
 & -I
\end{bmatrix}
F(\begin{bmatrix}
I & \\
 & -I
\end{bmatrix}
\begin{bmatrix} x \\ p \end{bmatrix}
) = - F(\begin{bmatrix} x \\ p \end{bmatrix}) $$
The detailed balance condition is satisfied if
$$F(x, p) =
\begin{bmatrix}
 \nabla_p \mathcal{H}(x, p) \\
 -\nabla_x \mathcal{H}(x, p)
\end{bmatrix} =
\begin{bmatrix}
 p \\
 \nabla_x L(x)
\end{bmatrix} .
$$
We have
$$ \nabla_p \mathcal{H}(x, -p) = - \nabla_p \mathcal{H}(x, p)$$
$$ -I(-\nabla_x \mathcal{H}(x, -p)) = -(-\nabla_x \mathcal{H}(x, p)) $$
$$\Rightarrow \nabla_x \mathcal{H}(x, -p) = \nabla_x \mathcal{H}(x, p) $$
This shows that there must be a certain symmetry in the momentum, which is true for our choice of Hamiltonian because $\nabla_p \mathcal{H}(x, p) = p$ has odd symmetry and $\nabla_x \mathcal{H}(x, p) = \nabla_x L(x)$ is independent of $p$.

**Conjecture: If the symplectic integrators that we use to numerically solve the ODEs in between jumps has similar properties (time reversibility, energy conserving, and volume preserving). Then, the numerical solution is the exact solution to a Hamiltonian ODE with a perturbed Hamiltonian function $\hat{\mathcal{H}}(x, p)$. Therefore, the numerical simulation scheme also satisfies detailed balance.**

## Stationary solution is canonical density (assuming existence and uniqueness)
We assume that the given function $L(x)$ and $\mathcal{H}(x, p)$ are sufficiently smooth so that the Hessian matrix is symmetric. Continuing from above, we want to show that
$$ -\nabla_{x,p} \cdot(F(x, p)\pi(x, p)) = 0 $$

For a Hamiltonian system, the above equation becomes
$$ \nabla_x \cdot (\pi(x, p) \nabla_p \mathcal{H}(x, p)) =
 \nabla_p \cdot (\pi(x, p)\nabla_x \mathcal{H}(x, p))
$$
$$\Rightarrow \nabla_x (\pi(x, p))\cdot  \nabla_p \mathcal{H}(x, p)
=
 \nabla_p \pi(x, p)\cdot  \nabla_x \mathcal{H}(x, p)
$$
Finally writting $\pi(x, p) \propto \exp(-U(x, p))$ then
$$ -\exp(-U(x, p))\nabla_x U(x, p) \cdot  \nabla_p \mathcal{H}(x, p)
= - \exp(-U(x, p))\nabla_p U(x, p) \cdot  \nabla_x \mathcal{H}(x, p) $$
$$\Rightarrow \nabla_x U(x, p) \cdot  \nabla_p \mathcal{H}(x, p)
= \nabla_p U(x, p) \cdot  \nabla_x \mathcal{H}(x, p) .$$
Clearly this is satisfied if $U(x, p) = \mathcal{H}(x, p).$

# Example of Hamiltonian velocity jump MCMC in Python

```python
Nsteps = 1000
N_burn_in = 100
Nchains = 8
lam = 0.7
dt = 0.1

## initial guess
x = 0.*ones((Nchains, 2))

## Burn in phase
for n in arange(N_burn_in):
    p0 = normal(0, 1, [Nchains, 2])
    p = p0.copy()
    tau = -log(rand(1))/lam ## generate an exponential RV for time
    K = tau//dt
    for k in arange(K):
        _p = p + 0.5*dt*gradL(x)
        x += dt*_p
        p = _p + 0.5*dt*gradL(x)

Xchain = zeros((Nsteps, Nchains, 2))
Xchain[0, ...] = x ## use the final value from burin-in phase as new initial guess
probs = zeros((Nsteps, Nchains))
acceptances = ones((Nsteps, Nchains))
Kmean = 0.
for n in arange(Nsteps-1):
    p0 = normal(0, 1, [Nchains, 2])
    p = p0.copy()
    x0 = Xchain[n, ...]
    x = x0.copy()
    tau = -log(rand(1))/lam ## generate an exponential RV for time
    K = tau//dt
    Kmean += K
    for k in arange(K):
        ## forward Euler
        #gL = gradL(x)
        #x += dt*p
        #p += dt*gL
        ## Leap Frog
        _p = p + 0.5*dt*gradL(x)
        x += dt*_p
        p = _p + 0.5*dt*gradL(x)
    log_p_MH = H(x0, p0) - H(x, p) ## if our numerical integrator is perfect, this should always be zero
    probs[n, :] = p_MH
    u = rand(Nchains)
    chi = 1.*(log(u) < log_p_MH)
    #chi = 1.*(u < exp(log_p_MH))
    acceptances[n+1, :] = chi
    Xchain[n+1, ...] = x0*(1 - chi[:, None]) + x*chi[:, None]

figure(1, [8, 8])
pcolor(X, Y, exp(Z))
plot(Xchain[..., 0].flatten(), Xchain[..., 1].flatten(), 'r.', ms=1)
print('acceptance ratio:', acceptances.sum(axis=0)/Nsteps)
print('mean number of leap frog steps:', Kmean/Nsteps)
```

```python
tfp.mcmc.effective_sample_size(Xchain)
```

```python
print('mean:', Xchain.mean(axis=(0, 1)))
print('estimated covariance:\n', cov(Xchain.reshape(-1, 2).T))
print('true covariance:\n', inv(M))
```

# HMC using Tensorflow Probability

```python
Nsteps = 100
N_burn_in = 10
Nchains = 4
dt = 0.1

Xinit = 0.*ones((Nchains, 2))

kernel = tfp.mcmc.HamiltonianMonteCarlo(
    target_log_prob_fn=L,
    num_leapfrog_steps=10,
    step_size=dt)

Xchain = tfp.mcmc.sample_chain(
    num_results=Nsteps,
    num_burnin_steps=N_burn_in,
    current_state=Xinit,
    trace_fn=None,
    kernel=kernel,
    parallel_iterations=4)
```

```python
Nsteps = 1000
N_burn_in = 100
Nchains = 8

Xinit = 0.*ones((Nchains, 2))

# A = rand(2, 2)
A = array([
    [0.8, 0.05],
    [0.7, 0.5]])
# A = eye(2)

M = A.T@A

# @tf.function(
#     input_signature=(
#             tf.TensorSpec(shape=[None, 2], dtype=tf.float64),))
def Ltf(_x):
    xT = _x[..., None, :]
    x = _x[..., None]
    return -0.5*(xT@M@x)[..., 0, 0]

#@tf.function()
def sampler():
    def trace_everything(states, previous_kernel_results):
        return previous_kernel_results

#     kernel = tfp.mcmc.NoUTurnSampler(
#         target_log_prob_fn=Ltf,
#         max_tree_depth=8,
#         max_energy_diff=1000,
#         step_size=tf.cast(0.1, tf.float64))

    kernel = tfp.mcmc.HamiltonianMonteCarlo(
        target_log_prob_fn=Ltf,
        num_leapfrog_steps=10,
        step_size=tf.cast(0.1, tf.float64))

    adaptive_kernel = kernel

#     adaptive_kernel = tfp.mcmc.DualAveragingStepSizeAdaptation(
#         inner_kernel=kernel,
#         num_adaptation_steps=int(0.8 * N_burn_in),
#         target_accept_prob=tf.cast(0.7, tf.float64))

    # adaptive_kernel = tfp.mcmc.SimpleStepSizeAdaptation(
    #     kernel,
    #     num_adaptation_steps=int(0.8*N_burn_in))
    Xchain, trace = tfp.mcmc.sample_chain(
        num_results=Nsteps,
        num_burnin_steps=N_burn_in,
        current_state=Xinit,
        trace_fn=trace_everything,
        kernel=adaptive_kernel,
        parallel_iterations=4)
    return Xchain, trace
Xchain, trace = sampler()
```

```python
Xchain = array(Xchain)
x = linspace(-5, 5, 200)
y = linspace(-10, 10, 200)
X, Y = meshgrid(x, y)
Z = L(stack([X, Y], 2))
figure(1, [8, 8])
pcolor(X, Y, exp(Z))
plot(Xchain[..., 0].flatten(), Xchain[..., 1].flatten(), 'r.', ms=1)
```

```python
tfp.mcmc.effective_sample_size(Xchain)
```

    <tf.Tensor: shape=(4, 2), dtype=float64, numpy=
    array([[ 3.99956288,  3.16376633],
           [10.7001864 ,  4.56980592],
           [15.88772314,  8.95965217],
           [16.19831692, 10.99054299]])>

```python
print('mean:', Xchain.mean(axis=(0, 1)))
print('estimated covariance:\n', cov(Xchain.reshape(-1, 2).T))
print('true covariance:\n', inv(M))
```

    mean: [-0.42855335  0.85169467]
    estimated covariance:
     [[ 1.70430429 -2.4595395 ]
     [-2.4595395   7.00745146]]
    true covariance:
     [[ 1.89528992 -2.9273785 ]
     [-2.9273785   8.48189154]]

```python
array(trace.new_step_size).mean(), array(trace.new_step_size).std()
```

# Bijectors
See Week 12

```python

```

```python

```

```python

```

```python

```

```python

```


Физтех-Школа Прикладной математики и информатики (ФПМИ) МФТИ

---

Обучение нейрона с помощью функции потерь LogLoss

---

Напоминание

Почти любой алгоритм машинного обучения, решающий задачу *классификации* или *регрессии*, работает так:

1. (*стадия инициализации*) Задаются его **гиперпараметры**, то есть те величины, которые не "выучиваются" алгоритмом в процессе обучения самостоятельно
2. (*стадия обучения*) Алгоритм запускается на данных, **обучаясь** на них и меняя свои **параметры** (не путать с *гипер*параметрами) каким-то определённым образом (например, с помощью *метода градиентного спуска*), исходя из функции потерь (её называют *loss function*). Функция потерь, по сути, говорит, где и как ошибается модель
3.  (*стадия предсказания*) Модель готова, и теперь с помощью неё можно делать **предсказания** на новых объектах

В данном ноутбуке будет решаться задача **бинарной классификации** с помощью нейрона:
- *Входные данные*: матрица $X$ размера $(n, m)$ и столбец $y$ из нулей и единиц размера $(n, 1)$. Строкам матрицы соответствуют объекты, столбцам - признаки (то есть строка $i$ есть набор признаков (*признаковое описание*) объекта $x_i$).
- *Выходные данные*: столбец $\hat{y}$ из нулей и единиц размера $(n, 1)$ - предсказания алгоритма.

Модель нейрона в биологии и в deep learning:

Нейрон с сигмоидой

Снова рассмотрим нейрон с сигмоидой, то есть $$f(x) = \sigma(x)=\frac{1}{1+e^{-x}}$$ на рисунке выше.

На предыдущем занятии мы установили, что **обучение нейрона с сигмоидой с квадратичной функцией потерь**:

$$MSE(w, x) = \frac{1}{n}\sum_{i=1}^{n} (\hat{y_i} - y_i)^2 = \frac{1}{n}\sum_{i=1}^{n} (\sigma(w \cdot x_i) - y_i)^2$$

где $w \cdot x_i$ - скалярное произведение, а $\sigma(w \cdot x_i) =\frac{1}{1+e^{-w \cdot x_i}} $ - сигмоида - **неэффективно**, то есть мы увидели, что даже за большое количество итераций нейрон предсказывает плохо.

Давайте ещё раз взглянем на формулу для градиентного спуска от функции потерь $J$ по весам нейрона:

$$ \frac{\partial Loss}{\partial w} = \frac{1}{n} X^T (\sigma(w \cdot X) - y)\sigma(w \cdot X)(1 - \sigma(w \cdot X))$$

А теперь смотрим на график сигмоиды:

**Её значения: числа от 0 до 1.**

Если получше проанализировать формулу, то теперь можно заметить, что, поскольку сигмоида принимает значения между 0 и 1 (а значит (1-$\sigma$) тоже принимает значения от 0 до 1), то мы умножаем $X^T$ на столбец $(\sigma(w \cdot X) - y)$ из чисел от -1 до 1, а потом ещё на столбцы $\sigma(w \cdot X)$ и $(1 - \sigma(w \cdot X))$ из чисел от 0 до 1. Таким образом в лучшем случае $\frac{\partial{J}}{\partial{w}}$ будет столбцом из чисел, порядок которых максимум 0.01 (в среднем, понятно, что если сигмоида выдаёт все 0, то будет 0, если все 1, то тоже 0). После этого мы умножаем на шаг градиентного спуска, который обычно порядка 0.001 или 0.01 максимум. То есть мы вычитаем из весов числа порядка ~0.0001. Медленновато спускаемся, не правда ли? Это называют **проблемой затухающих градиентов**.

Всё верно. В задачах классификации, в которых моделью является нейрон с сигмоидной функцией активации, предсказывающий "вероятности" принадлженостей к классам, почти никогда не используют квадратичную функцию потерь $J$. Вместо неё придумали другую прекрасную функцию потерь -- **LogLoss**:

$$LogLoss(\hat{y}, y) = -\frac{1}{n} \sum_{i=1}^n y_i \log(\hat{y_i}) + (1 - y_i) \log(1 - \hat{y_i}) = -\frac{1}{n} \sum_{i=1}^n y_i \log(\sigma(w \cdot x_i)) + (1 - y_i) \log(1 - \sigma(w \cdot x_i))$$

где, как и прежде, $y$ - столбец $(n, 1)$ из истинных значений классов, а $\hat{y}$ - столбец $(n, 1)$ из предсказаний нейрона.

```python
from matplotlib import pyplot as plt
from matplotlib.colors import ListedColormap
import numpy as np
import pandas as pd
```

```python
def loss(y_pred, y):
    return -np.mean(y * np.log(y_pred) + (1 - y) * np.log(1 - y_pred))
```

Отметим, что сейчас речь идёт именно о **бинарной классификации (на два класса)**, в многоклассовой классификации используется функция потерь под названием *кросс-энтропия*, которая является обобщением LogLoss'а на случай нескольких классов.

Почему же теперь всё будет лучше? Раньше была проблема умножения маленьких чисел в градиенте. Давайте посмотрим, что теперь:

* Для веса $w_j$:

$$ \frac{\partial Loss}{\partial w_j} =
-\frac{1}{n} \sum_{i=1}^n \left(\frac{y_i}{\sigma(w \cdot x_i)} - \frac{1 - y_i}{1 - \sigma(w \cdot x_i)}\right)(\sigma(w \cdot x_i))_{w_j}' = -\frac{1}{n} \sum_{i=1}^n \left(\frac{y_i}{\sigma(w \cdot x_i)} - \frac{1 - y_i}{1 - \sigma(w \cdot x_i)}\right)\sigma(w \cdot x_i)(1 - \sigma(w \cdot x_i))x_{ij} = $$
$$-\frac{1}{n} \sum_{i=1}^n \left(y_i - \sigma(w \cdot x_i)\right)x_{ij}$$

* Градиент $Loss$'а по вектору весов -- это вектор, $j$-ая компонента которого равна $\frac{\partial Loss}{\partial w_j}$ (помним, что весов всего $m$):

$$\begin{align}
    \frac{\partial Loss}{\partial w} &= \begin{bmatrix}
           -\frac{1}{n} \sum_{i=1}^n \left(y_i - \sigma(w \cdot x_i)\right)x_{i1} \\
           -\frac{1}{n} \sum_{i=1}^n \left(y_i - \sigma(w \cdot x_i)\right)x_{i2} \\
           \vdots \\
           -\frac{1}{n} \sum_{i=1}^n \left(y_i - \sigma(w \cdot x_i)\right)x_{im}
         \end{bmatrix}
\end{align}=\frac{1}{n} X^T \left(\hat{y} - y\right)$$

По аналогии с нейроном выведите формулу для свободного члена (bias'а) $b$:

$$ \frac{\partial Loss}{\partial b} =
-\frac{1}{n} \sum_{i=1}^n \left(\frac{y_i}{\sigma(w \cdot x_i + b)} - \frac{1 - y_i}{1 - \sigma(w \cdot x_i + b)}\right)(\sigma(w \cdot x_i + b))_{b}' = -\frac{1}{n} \sum_{i=1}^n \left(\frac{y_i}{\sigma(w \cdot x_i + b)} - \frac{1 - y_i}{1 - \sigma(w \cdot x_i + b)}\right)\sigma(w \cdot x_i + b)(1 - \sigma(w \cdot x_i + b))*1 = $$
$$-\frac{1}{n} \sum_{i=1}^n \left(y_i - \sigma(w \cdot x_i)\right)$$

Получили новое правило для обновления. Заметим, что это в точности то правило обновления весов для градиентного спуска, которое мы использовали для перцептрона (только другая функция активации). Получается, что мы пришли к этому правилу "по-честному", сделав функцией активации сигмоиду и введя новую функию потерь -- LogLoss, а когда реализовывали перцептрон, мы просто сказали (воспользовавшись *правилом Хебба*), что $f'(x)$ возьмём единицей, то есть тут имеет место интересная связь градиентного спуска и метода коррекции ошибки.

Отсюда очевидно, что код для нейрона с такой функцией потерь не будет ничем отличаться от кода для перцептрона, за исключением метода `self.activate()` и самого подсчёта `loss()`. Напишем:

```python
def sigmoid(x):
    """Сигмоидальная функция"""
    return 1 / (1 + np.exp(-x))
```

Реализуем нейрон с функцией потерь LogLoss:

```python
class Neuron:

    def __init__(self, w=None, b=0):
        """
        :param: w -- вектор весов
        :param: b -- смещение
        """
        # пока что мы не знаем размер матрицы X, а значит не знаем, сколько будет весов
        self.w = w
        self.b = b

    def activate(self, x):
        <Ваш код здесь>

    def forward_pass(self, X):
        """
        Эта функция рассчитывает ответ нейрона при предъявлении набора объектов
        :param: X -- матрица объектов размера (n, m), каждая строка - отдельный объект
        :return: вектор размера (n, 1) из нулей и единиц с ответами перцептрона
        """
        <Ваш код здесь>

    def backward_pass(self, X, y, y_pred, learning_rate=0.1):
        """
        Обновляет значения весов нейрона в соответствие с этим объектом
        :param: X -- матрица объектов размера (n, m)
                y -- вектор правильных ответов размера (n, 1)
                learning_rate - "скорость обучения" (символ alpha в формулах выше)
        В этом методе ничего возвращать не нужно, только правильно поменять веса
        с помощью градиентного спуска.
        """
        <Ваш код здесь>

    def fit(self, X, y, num_epochs=5000):
        """
        Спускаемся в минимум
        :param: X -- матрица объектов размера (n, m)
                y -- вектор правильных ответов размера (n, 1)
                num_epochs -- количество итераций обучения
        :return: J_values -- вектор значений функции потерь
        """
        self.w = np.zeros((X.shape[1], 1))  # столбец (m, 1)
        self.b = 0  # смещение
        Loss_values = []  # значения функции потерь на различных итерациях обновления весов

        for i in range(num_epochs):
            <Ваш код здесь>

        return Loss_values
```

Тестирование

Протестируем нейрон, обученный с новой функцией потерь, на тех же данных, что и в предыдущем ноутбуке:

**Проверка forward_pass()**

```python
w = np.array([1., 2.]).reshape(2, 1)
b = 2.
X = np.array([[1., 3.],
              [2., 4.],
              [-1., -3.2]])

neuron = Neuron(w, b)
y_pred = neuron.forward_pass(X)
print ("y_pred = " + str(y_pred))
```

**Проверка backward_pass()**

```python
y = np.array([1, 0, 1]).reshape(3, 1)
```

```python
neuron.backward_pass(X, y, y_pred)

print ("w = " + str(neuron.w))
print ("b = " + str(neuron.b))
```

Загрузим данные (яблоки и груши):

```python
data = pd.read_csv("./data/apples_pears.csv")
```

```python
data.head()
```

```python
plt.figure(figsize=(10, 8))
plt.scatter(data.iloc[:, 0], data.iloc[:, 1], c=data['target'], cmap='rainbow')
plt.title('Яблоки и груши', fontsize=15)
plt.xlabel('симметричность', fontsize=14)
plt.ylabel('желтизна', fontsize=14)
plt.show();
```

Обозначим, что здесь признаки, а что - классы:

```python
X = data.iloc[:,:2].values  # матрица объекты-признаки
y = data['target'].values.reshape((-1, 1))  # классы (столбец из нулей и единиц)
```

**Вывод функции потерь**
Функция потерь должна убывать и в итоге стать близкой к 0

```python
%%time
neuron = <Ваш код здесь>
J_values = <Ваш код здесь>

plt.figure(figsize=(10, 8))
plt.plot(J_values)
plt.title('Функция потерь', fontsize=15)
plt.xlabel('номер итерации', fontsize=14)
plt.ylabel('$J(\hat{y}, y)$', fontsize=14)
plt.show()
```

Посмотрим, как теперь нейрон классифицировал объекты из выборки:

```python
plt.figure(figsize=(10, 8))
plt.scatter(data.iloc[:, 0], data.iloc[:, 1], c=np.array(neuron.forward_pass(X) > 0.5).ravel(), cmap='spring')
plt.title('Яблоки и груши', fontsize=15)
plt.xlabel('симметричность', fontsize=14)
plt.ylabel('желтизна', fontsize=14)
plt.show();
```

Видим, что с LogLoss в случае классификации работать лучше, чем с квадратичной функцией потерь (с 5000 итераций и lr=0.1 здесь лучше, чем с 5000 итераций и lr=0.1 в нейроне с MSE).

Полезные ссылки

0). Статья от Стэнфорда: http://cs231n.github.io/neural-networks-1/

1). Хорошая статья про функции активации: https://www.jeremyjordan.me/neural-networks-activation-functions/

2). [Видео от Siraj Raval](https://www.youtube.com/watch?v=-7scQpJT7uo)

3). Современная статья про функции активации. Теперь на хайпе активация **$swish(x) = x\sigma (\beta x)$:** https://arxiv.org/pdf/1710.05941.pdf (кстати, при её поиске в некоторой степени использовался neural architecture search)

4). **SeLU** имеет очень интересные, доказанные с помощью теории вероятностей свойства: https://arxiv.org/pdf/1706.02515.pdf (да, в этой статье 102 страницы)


```python
import sympy as sym
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
import control
# from ipywidgets import interact

s = sym.symbols('s', complex=True)

```

# Diseño de controladores mediante Lugar Geométrico de las Raíces

Se busca encontrar compensadores a partir de análisis del **Lugar Geométrico de las Raíces**.

Los compensadores se diseñan para que los procesos desarrollen tareas específicas cumpliendo con especificaciones del comportamiento en la respuesta transitoria (tiempo de estabilización, máxima sobreelongación, etc) y estacionaria (error de estado estable).

## Método

Se añaden polos y ceros (del compensador) a la función de transferencia del sistema en lazo directo $G(s)$, de manera que el **Lugar Geométrico de las Raíces** pase por los polos deseados en lazo cerrado. Una vez se garantiza que el **Lugar Geométrico de las Raíces** pasa por los polos deseados, se encuentra la ganancia del compensador que permite que el sistema en lazo cerrado se comporte según los polos deseados.

Tenga en cuenta que la función de transferencia de lazo cerrado, en un sistema controlado con realimentación unitaria, es:

$$G_{LC} = \frac{G(s)}{1+G(s)}$$

Los valores de $s$ que conforman el Lugar Geométrico de las Raíces cumplen que:

$$1+G(s) = 0$$

Que es equivalente a:

$$G(s) = -1$$

De donde se genera la condición de fase:

$$\angle G(s) = \pm 180 ^o$$

Y la condición de magnitud:

$$ | G(s) | = 1$$

### Compensador en adelanto

Los pasos para diseñar un compensador mediante el Lugar Geométrico de las Raíces son:

1. Determine la localización deseada para los polos dominantes en lazo cerrado.

2. A partir del Lugar Geométrico de las Raíces, determine si un ajuste de ganancia puede brindar el comportamiento deseado. Si no, encuentre el ángulo que debe aportar el compensador para que el Lugar Geométrico de las Raíces pase por los polos deseados.

3. Suponga la forma de un compensador

$$ G_c(s) = k_c\frac{s+\frac{1}{T}}{s+\frac{1}{\alpha T}}$$

donde los parámetros $k_c$, $\alpha$ y $T$ se determinan a partir de los requisitos del sistema.

4. Encuentre $\alpha$ y $T$ para garanttizar que el Lugar Geométrico de las Raíces pase por los polos deseados con la condición de fase.

5. Encuentre $k_c$ a partir de la condición de magnitud.

**Ejemplo**

Suponga un proceso cuyo modelo es:

$$G_p(s) = \frac{4}{s\left ( s+2 \right )}$$

```python
# Se define la función de transferencia del proceso
Gp = control.tf(4, [1,2,0])
Gp
```

$$\frac{4}{s^2 + 2 s}$$

Si se realimenta el sistema con un compensador $G_c(s)=1$

```python
G_LC1 = control.feedback(Gp,1)
G_LC1
```

$$\frac{4}{s^2 + 2 s + 4}$$

Los polos y ceros del sistema en lazo cerrado serían:

```python
# Polos de lazo con Gc(s) = 1
pLC1 = G_LC1.pole()
pLC1
```

    array([-1.+1.73205081j, -1.-1.73205081j])

```python
# Ceros de lazo con Gc(s) = 1
cLC1 = G_LC1.zero()
cLC1
```

    array([], dtype=float64)

```python
tfin = 20
dt = 1/10
ts = np.linspace(0, tfin, round(tfin/dt))

_, yLC1 = control.step_response(G_LC1,ts)
plt.plot(ts,yLC1,
         ts,0.98*np.ones(len(ts)),
         ts,1.02*np.ones(len(ts)))
plt.grid()
```

Suponga que requiere disminuir el tiempo de estabilización a la mitad del que obtendría al realimenta con $G_c(s)=1$ manteniendo el amortiguamiento.

```python
# Polos de lazo cerrado deseados
# pLC_deseado = 2*pLC1
# pLC_deseado
pLC_deseado = np.array([-2 + 2*1j,-2 - 2*1j])
pLC_deseado
```

    array([-2.+2.j, -2.-2.j])

Se verifica si los polos deseados hacen parte del Lugar Geométrico de las Raíces.

```python
_,_ = control.root_locus(Gp,#FdT proceso
                         plot = True);
plt.plot(pLC_deseado.real, pLC_deseado.imag, 'r+')
```

Como los polos deseados no hacen parte del Lugar Geométrico de las Raíces, se define una forma de compensador.

$$ G_c(s) = k_c\frac{s+\frac{1}{T}}{s+\frac{1}{\alpha T}}$$

y la función de transferencia de lazo directo:

$$ G(s) = G_c(s)G_p(s)$$

```python
G_p_sym = 4/(s**2+2*s)#FdT simbolica proceso
G_p_sym
```

$\displaystyle \frac{4}{s^{2} + 2 s}$

Al evaluar la función de transferencia del proceso en los polos deseados se obtiene:

```python
pLC_deseado
```

    array([-2.+2.j, -2.-2.j])

```python
#FdT proceso evaluada en un polo deseado
G_p_sym_des = G_p_sym.subs(s,pLC_deseado[0]).simplify()
G_p_sym_des
```

$\displaystyle - \frac{1}{2} + \frac{i}{2}$

Este número complejo equivale a una magnitud

```python
#norma de FdT proceso evaluada en un polo deseado

sym.Abs(G_p_sym_des)
```

$\displaystyle \frac{\sqrt{2}}{2}$

y una fase

```python
#fase de FdT proceso evaluada en un polo deseado

sym.arg(G_p_sym_des)
```

$\displaystyle \frac{3 \pi}{4}$

La condición de fase indica que el compensador debe aportar un ángulo que lleve el ángulo completo a $\pm 180^o$, en este caso:

```python
#angulo que debe aportar el compensador para cumplir cond. de fase
ang_comp = -(sym.arg(G_p_sym_des) - sym.pi)
ang_comp
```

$\displaystyle \frac{\pi}{4}$

por lo que se requiere un compensador.  Se propone usar

$$ G_c(s) = k_c\frac{s+\frac{1}{T}}{s+\frac{1}{\alpha T}}$$

```python
#FdT simbolica del compensador

kc,T,alpha,beta = sym.symbols('k_c,T,alpha,beta', real=True,positive = True)

G_c_sym = kc*(s+1/T)/(s+1/(alpha*T))
G_c_sym
```

$\displaystyle \frac{k_{c} \left(s + \frac{1}{T}\right)}{s + \frac{1}{T \alpha}}$

El ángulo que aporta el compensador es:

```python
sym.arg(G_c_sym.subs(s,pLC_deseado[0]).simplify())
```

$\displaystyle \arg{\left(\frac{2.0 T \left(-1 + i\right) + 1}{2.0 T \alpha \left(-1 + i\right) + 1} \right)}$

Observe que tiene dos parámetros y una sola condición. En este caso se selecciona un parámetro de forma arbitraria para determinar el otro.

Una posible solución es ubicar el cero del compensador bajo el polo deseado.

El cero del compensador cumple:
$$s+\frac{1}{T} = 0$$
Como se desea en $s=-2$
$$-2+\frac{1}{T} = 0$$
$$T = \frac{1}{2}$$

y se soluciona la condición de fase

```python
parametros = {T:1/2}
```

```python
CondFase = sym.arg(G_c_sym.subs(s,pLC_deseado[0]).
                   subs(parametros))-ang_comp
CondFase
```

$\displaystyle \arg{\left(\frac{i}{-1 + i + \frac{1}{\alpha}} \right)} - \frac{\pi}{4}$

```python
CondFase = sym.arg(G_c_sym.subs(s,pLC_deseado[0]).
                   subs(parametros))-ang_comp
sym.solve(CondFase,alpha)

```

    [1/2]

```python
parametros = {T:1/2, alpha:0.5}
```

```python
#FdT simbolica del compensador con los parametros hasta el momento

G_c_sym.subs(parametros)
```

$\displaystyle \frac{k_{c} \left(s + 2.0\right)}{s + 4.0}$

Así, el compensador es:

$$G_c(s) = k_c \frac{s+2}{s+4}$$

Y falta encontrar $k_c$

```python
#FdT  del compensador para simular

Gc = control.tf([1,2],[1,4])
Gc
```

$$\frac{s + 2}{s + 4}$$

```python
G = Gc*Gp
G
```

$$\frac{4 s + 8}{s^3 + 6 s^2 + 8 s}$$

El Lugar Geométrico de las Raíces del proceso y el compensador es:

```python
_,_ = control.root_locus(Gc*Gp,
                         plot = True);
plt.plot(pLC_deseado.real, pLC_deseado.imag, 'r+')
```

Ahora debe hallarse $k_c$ con la condición de magnitud.

Ya se ha calculado que la magnitud del proceso evaluado en el polo deseado es:

```python
sym.Abs(G_p_sym_des)
```

$\displaystyle \frac{\sqrt{2}}{2}$

Por lo que el compensador debe aportar una magnitud de:

```python
# mag_comp * sym.Abs(G_p_sym_des) = 1

```

```python
mag_comp = 1/sym.Abs(G_p_sym_des)
#magnitud que debe tener el compensador para cumplir cond.
```

```python
CondMag = sym.Abs(G_c_sym.subs(s,pLC_deseado[0]).
                   subs(parametros))/mag_comp - 1
sym.solve(CondMag,kc)
```

    [2.00000000000000]

```python
parametros = {T:1/2, alpha:0.5, kc: 2}
```

Y  el compensador es:

```python
G_c_sym.subs(parametros)
```

$\displaystyle \frac{2 \left(s + 2.0\right)}{s + 4.0}$

```python
Gc = 2*control.tf([1,2],[1,4])
Gc
```

$$\frac{2 s + 4}{s + 4}$$

Verifique el comportamiento en el dominio del tiempo

**Respuesta al escalón del proceso sin compensador**

```python
tfin = 20
dt = 1/10
ts = np.linspace(0, tfin, round(tfin/dt))

_, yLA = control.step_response(Gp, ts)
plt.plot(ts,yLA)
plt.grid()

```

**Respuesta al escalón del proceso realimentado**

```python

_, yLC1 = control.step_response(control.feedback(Gp,1),ts)
plt.plot(ts,yLC1)
plt.grid()

```

**Respuesta al escalón del proceso con compensador**

```python

_, yLC = control.step_response(control.feedback(Gc*Gp,1),ts)
plt.plot(ts,yLC)
plt.grid()

```

Puede compararse la respuesta del sistema solo realimentado con la respuesta del sistema compensado

```python
plt.plot(ts,yLC,ts,yLC1)
plt.legend(('Compensado','Realimentado'))
plt.grid()

```

Observe que el sistema controlado tiene un **error de estado estale** nulo para una entrada escalón. ¿Qué ocurre si la referencia es una rampa?

```python
tfin = 10
dt = 1/10
ts = np.linspace(0, tfin, round(tfin/dt))

_, yLCr = control.step_response(control.feedback(Gc*Gp,1)*
                                control.tf(1,[1,0]),ts)
plt.plot(ts,yLCr)
plt.plot([ts[0],ts[-1]], [0,ts[-1]])
plt.legend(('Com. Adelanto','Referencia'))
plt.grid()
```

¿Cómo disminuir el error en estado estable sin afectar drásticamente la respuesta transitoria?

Suponga que entre los requerimientos también se encuentra "reducir" el error de estado estable ante una entrada más elaborada.  En este caso, se deben agregar dinámicas que no alteren de forma significativa al **LGDR** (porque ya se logró un requerimiento con el controlador en adelanto).

### Compensador en retardo

Los pasos para diseñar un compensador en retardo mediante el Lugar Geométrico de las Raíces son:

1. Suponga la forma de un compensador en retardo

$$ G_c(s) = k_c\frac{s+\frac{1}{T}}{s+\frac{1}{\beta T}}$$

donde los parámetros $k_c$, $T$ y $\beta$ se determinan a partir de los requisitos del sistema.

Observe que

$$ \lim_{s\rightarrow 0}{G_c(s)} = k_c \cdot \beta$$

2. Encuentre la constante de error de estado estable especificada según los requerimientos.

3. Determine el incremento necesario en la constante de error estático.

4. Determine $T$ y $\beta$ apropiados para cumplir el incremento anterior sin alterar el **LGDR**.

5. Encuentre $k_c$ a partir de la condición de magnitud para ajustar la posición de los polos dominantes.

```python
plt.plot(ts,ts-yLCr,
         label = 'Error a rampa')
plt.legend()
plt.grid()
Gc*Gp
```

De acuerdo con la gráfica anterior, el error de estado estable ante una entrada de velocidad es aproximadamente $0.25$.

$$E_{ss}=\frac{1}{\lim_{\rightarrow 0}sG(s)}$$

```python
Gc*Gp
```

$$\frac{16 s + 32}{s^3 + 6 s^2 + 8 s}$$

```python
8/32
```

    0.25

El error de estado estable ante la rampa puede disminuirse a $0.025$, por ejemplo. Para lograr esto, se selecciona $\beta = 10$, entonces el compensador en retardo es:

$$ k_c\frac{s+\frac{1}{T}}{s+\frac{1}{10 T}}$$

Como se requiere que el compensador no altere de manera significativa el **LGDR**, se toma $T$ de manera que su polo y cero estén "cercanos", por ejemplo $40$, por lo que el controlador es:

$$ k_c\frac{s+\frac{1}{40}}{s+\frac{1}{400}} = k_c\frac{s+0.025}{s+0.0025}$$

Verifique que el **LGDR** no se ha alterado significativamente

```python
G_at = control.tf([1,0.025],[1,0.0025])

_,_ = control.root_locus(Gc*Gp,plot = True);
_,_ = control.root_locus(G_at*Gc*Gp,plot = True);
plt.plot(pLC_deseado.real, pLC_deseado.imag, 'r+')
```

```python
G_at_sym = kc*(s+1/T)/(s+1/(beta*T))
G_at_sym
```

$\displaystyle \frac{k_{c} \left(s + \frac{1}{T}\right)}{s + \frac{1}{T \beta}}$

```python
GLA_sym = (G_at_sym.subs({T:40,
                          beta:10})*
           G_c_sym.subs({T:0.5,
                         alpha:0.5,
                         kc:4})*
           G_p_sym)
GLA_sym
```

$\displaystyle \frac{16 k_{c} \left(s + \frac{1}{40}\right) \left(s + 2.0\right)}{\left(s + \frac{1}{400}\right) \left(s + 4.0\right) \left(s^{2} + 2 s\right)}$

```python
sym.Abs(GLA_sym.subs(s,pLC_deseado[0])).simplify()
```

$\displaystyle 0.997201173495743 k_{c}$

```python
kc/sym.Abs(GLA_sym.subs(s,pLC_deseado[0])).simplify()
```

$\displaystyle 1.00280668192$

El compensador en atraso será entonces:

$$  1.003\frac{s+0.025}{s+0.0025}$$

El sistema compensado completo es:

```python
G_at = 1.003*control.tf([1,0.025],[1,0.0025])
G = G_at*Gc*Gp
G
```

$$\frac{16.05 s^2 + 32.5 s + 0.8024}{s^4 + 6.003 s^3 + 8.015 s^2 + 0.02 s}$$

```python

_, yLC_aa = control.step_response(control.feedback(G_at*Gc*Gp,1),ts)
plt.plot(ts,yLC1,
         ts,yLC,
         ts,yLC_aa)
plt.legend(('Solo realimentado','Comp. adelanto','Comp. ad. at.'))
plt.grid()

```

```python
## Respuestas ante la rampa
_, yLC_aar = control.step_response(control.feedback(G_at*Gc*Gp,1)*
                                  control.tf(1,[1,0]),ts)
plt.plot(ts,ts,
         ts,yLCr,
         ts,yLC_aar)
plt.legend(('rampa','Comp. adelanto','Comp. ad. at.'))
plt.grid()
```

```python
## error ante la rampa

plt.plot(ts,ts-yLCr,
         ts,ts-yLC_aar)
plt.legend(('Comp. adelanto','Comp. ad. at.'))
plt.grid()
```

-----------------------
**Ejercicio 1**

Considere el proceso modelado por

$$G_p(s) =  \frac{5}{s(s+4)(s+10)}$$

Encuentre un compensador para que los polos dominantes en lazo cerrado estén en $-3 \pm j3$

- No es necesario poner un cero bajo el polo deseado.
- Pruebe poniendo el cero a la derecha (valor real mayor) del polo deseado.
- Pruebe poniendo el cero a la izquierda (valor real menor) del polo deseado.
- Disminuya a la mitad el error de estado estable ante la rampa.

**Ejercicio 2**

Revise el contenido de la siguiente URL.
[https://homes.esat.kuleuven.be/~tokka/daisydata.html](https://homes.esat.kuleuven.be/~tokka/daisydata.html)

Analice los datos del proceso **dryer**

¿cómo puede hallar un modelo para el proceso?
- Diseñe un compesador para mejorar el funcionamiento del sistema

```python
import pandas as pd

datos = pd.read_csv("Datos/dryer.dat",sep="\t",header = None)
ts = 20
Entrada = list(datos[0]-datos[0].mean())
Salida = list(datos[1]-datos[1].mean())
tiempo = list(range(0,ts*(len(Entrada)),ts))

```

```python
plt.plot(tiempo[0:500],Entrada[0:500])
```

```python
plt.plot(tiempo[0:500],Salida[0:500])
```

```python
n = len(Entrada)
Entrada_1 = Entrada.copy()
Entrada_1.insert(0, 0)

Entrada_2 = Entrada_1.copy()
Entrada_2.insert(0, 0)

Salida_1 = Salida.copy()
Salida_1.insert(0, 0)

Salida_2 = Salida_1.copy()
Salida_2.insert(0, 0)

df = pd.DataFrame(np.transpose(np.array([Entrada[0:n],
                                         Entrada_1[0:n],
                                         Entrada_2[0:n],
                                         Salida[0:n],
                                         Salida_1[0:n],
                                         Salida_2[0:n]])),
                  columns=['U(t)',
                           'U(t-Ts)',
                           'U(t-2Ts)',
                           'Y(t)',
                           'Y(t-Ts)',
                           'Y(t-2Ts)'])

df["dUdt"] = (df["U(t)"]-df["U(t-Ts)"])/ts
df["dYdt"] = (df["Y(t)"]-df["Y(t-Ts)"])/ts

plt.plot(df.dYdt)
```

```python

```