```python
import numpy as np
import sympy as sm
import scipy as sp
from scipy import optimize
from scipy import interpolate

import matplotlib.pyplot as plt
import ipywidgets as widgets
from mpl_toolkits.mplot3d import Axes3D
%matplotlib inline
```

# 1. Human capital accumulation

Consider a worker living in **two periods**, $t \in \{1,2\}$.

In each period she decides whether to **work ($l_t = 1$) or not ($l_t = 0$)**.

She can *not* borrow or save and thus **consumes all of her income** in each period.

If she **works** her **consumption** becomes:

$$c_t = w h_t l_t\,\,\text{if}\,\,l_t=1$$

where $w$ is **the wage rate** and $h_t$ is her **human capital**.

If she does **not work** her consumption becomes:

$$c_t = b\,\,\text{if}\,\,l_t=0$$

where $b$ is the **unemployment benefits**.

Her **utility of consumption** is:

$$ \frac{c_t^{1-\rho}}{1-\rho} $$

Her **disutility of working** is:

$$ \gamma l_t $$

From period 1 to period 2, she **accumulates human capital** according to:

$$ h_2 = h_1 + l_1 +
\begin{cases}
0 & \text{with prob. }0.5 \\
\Delta & \text{with prob. }0.5
\end{cases} \\
$$

where $\Delta$ is a **stochastic experience gain**.

In the **second period** the worker thus solves:

$$
\begin{eqnarray*}
v_{2}(h_{2}) & = &\max_{l_{2}} \frac{c_2^{1-\rho}}{1-\rho} - \gamma l_2
\\ & \text{s.t.} & \\
c_{2}& = & \begin{cases}
w h_2 &
\text{if }l_2 = 1  \\
b & \text{if }l_2 = 0
\end{cases} \\
l_{2}& \in &\{0,1\}
\end{eqnarray*}
$$

In the **first period** the worker thus solves:

$$
\begin{eqnarray*}
v_{1}(h_{1}) &=& \max_{l_{1}} \frac{c_1^{1-\rho}}{1-\rho} - \gamma l_1 + \beta\mathbb{E}_{1}\left[v_2(h_2)\right]
\\ & \text{s.t.} & \\
c_{1}& = & \begin{cases}
w h_1 &
\text{if }l_1 = 1  \\
b & \text{if }l_1 = 0
\end{cases} \\
h_2 &=& h_1 + l_1 + \begin{cases}
0 & \text{with prob. }0.5\\
\Delta & \text{with prob. }0.5
\end{cases}\\
l_{1} &\in& \{0,1\}\\
\end{eqnarray*}
$$

where $\beta$ is the **discount factor** and $\mathbb{E}_{1}\left[v_2(h_2)\right]$ is the **expected value of living in period two**.

The **parameters** of the model are:

```python
rho = 2
beta = 0.96
gamma = 0.1
w = 2
b = 1
Delta = 0.1
```

The **relevant levels of human capital** are:

```python
h_vec = np.linspace(0.1, 1.5, 100)
```

**Question 1:** Solve the model in period 2 and illustrate the solution (including labor supply as a function of human capital).

**Answer to Question 1:**

```python
# The basic functions are:

# Utility
def utility(c, rho):
    return c**(1-rho)/(1-rho)

# Consumption
def c(w, h, l, b):
    return w*h*l + b*(1-l)

# Disutility
def disutility(l, gamma):
    return gamma*l

# Total utility in period 2
def v2(w, h2, l2, b, rho, gamma):
    return utility(c(w, h2, l2, b), rho) - disutility(l2, gamma)

# Total utility in period 1
def v1(h1, l1, v2_interp, Delta, w, b, rho, gamma, beta):

    # From period 1 to period 2 the consumer can acummulate human capital based on the two functions below:

    # If human capital is not accumulated, utility in period 2 becomes
    N_h2 = h1 + l1 + 0
    N_v2 = v2_interp([N_h2])[0]

    # b. If human capital is accumulated, utility in period 2 becomes
    Y_h2 = h1 + l1 + Delta
    Y_v2 = v2_interp([Y_h2])[0]

    # c. Given the probabilities for ho
    v2 = 0.5*N_v2 + 0.5*Y_v2

    # d. total net utility
    return utility(c(w, h1, l1, b), rho) - disutility(l1, gamma) + beta*v2
```

**Note:** the consumer in period 2, decides whether to work or not by comparing her two utilities. If she is better by not working, then decides not to work $(l_{2}=0)$, otherwise chooses to work $(l_{2}=1)$.

```python
# The solution function for period 2 is:

def solve_period_2(rho, gamma, Delta):

    # Create the vectors for period 2
    l2_vec = np.empty(100)
    v2_vec = np.empty(100)

    # Solve for each h2 in grid
    for i, h2 in enumerate(h_vec):

        # Choose either l2 = 0 or l2 = 1, by comparing the utility values from these two options
        if v2(w, h2, 1, b, rho, gamma) <= v2(w, h2, 0, b, rho, gamma):
            l2_vec[i] = 0
        else:
            l2_vec[i] = 1

        # Save the estimated values of v2, based on the choice of working or not
        v2_vec[i] = v2(w, h2, l2_vec[i], b, rho, gamma)

    return l2_vec, v2_vec
```

```python
# Solve for period 2
l2_vec, v2_vec = solve_period_2(rho, gamma, Delta)

# Figure
fig = plt.figure(figsize = (8,10))
ax = fig.add_subplot(2,1,1)
ax1 = fig.add_subplot(2,1,2)
ax.plot(h_vec, l2_vec)
ax1.plot(h_vec, v2_vec)

# Labels
ax.set_xlabel('Human Capital')
ax.set_ylabel('Labor Supply')
ax.set_title('Labor Supply and Human Capital- Period 2')

ax1.set_xlabel('Human Capital')
ax1.set_ylabel('Utility')
ax1.set_title('Utility and Human Capital- Period 2')

plt.tight_layout()
```

**Conclusion:** from the above figure, we can see that the individual will choose not to work in period 2 whenever the accumulated human capital ($h_2{}$) is approximately below 0.6. If the accumulated human capital is above that level, she will get more utility by working in period 2, rather than not working.

**Question 2:** Solve the model in period 1 and illustrate the solution (including labor supply as a function of human capital).

**Answer to Question 2**

```python
# The solution function for period 1 is:

def solve_period_1(rho, gamma, beta, Delta, v1, v2_interp):

    # Vectors
    l1_vec = np.empty(100)
    v1_vec = np.empty(100)

    # Solve for each h1
    for i, h1 in enumerate(h_vec):

        # The individual decides whether to work or not by comparing his utility. If she is better off not working, we will have
        # l1=0 otherwise l1=1
        if v1(h1, 1, v2_interp, Delta, w, b, rho, gamma, beta) <= v1(h1, 0, v2_interp, Delta, w, b, rho, gamma, beta):
            l1_vec[i] = 0
        else:
            l1_vec[i] = 1

        v1_vec[i] = v1(h1, l1_vec[i], v2_interp, Delta, w, b, rho, gamma, beta)

    return l1_vec, v1_vec

# Construct interpolator
v2_interp = interpolate.RegularGridInterpolator((h_vec,), v2_vec, bounds_error=False, fill_value=None)
```

```python
# Solve for period 1
l1_vec, v1_vec = solve_period_1(rho, gamma, beta, Delta, v1, v2_interp)

# Figure
fig = plt.figure(figsize = (8,10))
ax = fig.add_subplot(2,1,1)
ax1 = fig.add_subplot(2,1,2)
ax.plot(h_vec,l1_vec)
ax1.plot(h_vec, v1_vec)

# Labels
ax.set_xlabel('Human Capital')
ax.set_ylabel('Labor Supply')
ax.set_title('Labor Supply and Human Capital - Period 1')

ax1.set_xlabel('Human Capital')
ax1.set_ylabel('Utility')
ax1.set_title('Utility and Human Capital- Period 1')

plt.tight_layout()
```

**Conclusion:** from this figure we can conclude that the individual will choose not to work whenever his human capital is aporximately below 0.4. If the accumulated human capital is larger than that, the individual is better off working. The human capital level that makes the individual work is lower in period 1 than in period 2, because the individual recognizes the potential experience gain in period 1, that could be tranfered in period 2.

**Question 3:** Will the worker never work if her potential wage income is lower than the unemployment benefits she can get? Explain and illustrate why or why not.

**Answer to Question 3:**

The straight forward answer to this question is no. Even if the wage income is lower than the unemployment benefits, we can see that there are still some high values of human capital that would make the individual better off if she decides to work. In order to verify this, we can set the level of unempoyment benefits "b" above the wage level "w" and re-run the code above.

For example, setting the unemployment benefits equal to 2.2 (w = 2) we can see that for a certain level of human capital, the individual would still choose to work in both periods. However, these levels of h are much higher now compared to the situation when b < w.

```python
b = 2.2
```

```python
# Solve for period 2 and period 1
# We did not create new vectors, just use the old ones
l2_vec, v2_vec = solve_period_2(rho, gamma, Delta)
l1_vec, v1_vec = solve_period_1(rho, gamma, beta, Delta, v1, v2_interp)

# Figure
fig = plt.figure(figsize = (12,8))
ax = fig.add_subplot(2,2,1)
ax1 = fig.add_subplot(2,2,2)
ax2 = fig.add_subplot(2,2,3)
ax3 = fig.add_subplot(2,2,4)

# Plots
ax.plot(h_vec,l2_vec)
ax1.plot(h_vec, l1_vec)
ax2.plot(h_vec, v2_vec)
ax3.plot(h_vec, v1_vec)

# Labels
ax.set_xlabel('Human Capital')
ax.set_ylabel('Labor Supply')
ax.set_title('Labor Supply and Human Capital - Period 2')

ax1.set_xlabel('Human Capital')
ax1.set_ylabel('Labor Supply')
ax1.set_title('Labor Supply and Human Capital - Period 1')

ax2.set_xlabel('Human Capital')
ax2.set_ylabel('Utility')
ax2.set_title('Utility and Human Capital- Period 2')

ax3.set_xlabel('Human Capital')
ax3.set_ylabel('Utility')
ax3.set_title('Utility and Human Capital- Period 1')

plt.tight_layout()
```

# 2. 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 **ex ante 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
par = {}

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

**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}$.

**Answer to question 1:**

```python
# Activating pretty printing
sm.init_printing(use_unicode=True)
```

```python
# Define all the needed variables for the symbolic solution
v = sm.symbols('v_t')
y_t = sm.symbols('y_t')
pi_t = sm.symbols('pi_t')
h = sm.symbols('h')
alpha = sm.symbols('alpha')
b = sm.symbols('b')

pi_t_1 = sm.symbols('\pi_{t-1}')
y_t_1 = sm.symbols('y_{t-1}')
s_t = sm.symbols('s_t')
s_t_1 = sm.symbols('s_{t-1}')
gamma = sm.symbols('gamma')
phi = sm.symbols('\phi')

# Check if they are printed properly
h, alpha, v, b, y_t, pi_t, pi_t_1, gamma, phi, y_t_1, s_t, s_t_1
```

```python
AD = sm.Eq(pi_t, (1/(h*alpha))*(v - (1+b*alpha)*y_t))
SRAS = sm.Eq(pi_t, (pi_t_1 + gamma*y_t - phi*gamma*y_t_1 + s_t - phi*s_t_1))
AD, SRAS
```

**Solving for the equilibrium value of output ($y_{t}$) in three steps:**

1. **Step 1:** Solve AD wrt $\pi_{t}$
2. **Step 2:** Substistute in SRAS equation
3. **Step 3:** Solve wrt $y_{t}$

```python
AD_1 = sm.solve(AD, pi_t)
AD_1
```

```python
AD_2 = SRAS.subs(pi_t, AD_1[0])
AD_2
```

```python
Output = sm.solve(AD_2, y_t)
Output
```

**Solving for the equilibrium value of inflation ($\pi_{t}$) in two steps**

1. **Step 1:** Substistute $y_{t}$ in SRAS
2. **Step 2:** Solve wrt $\pi_{t}$

```python
AS_1 = SRAS.subs(y_t, Output[0])
AS_1
```

```python
Inflation = sm.solve(AS_1, pi_t)
Inflation
```

```python
# Deactivating pretty printing
sm.init_printing(use_unicode=False)
```

**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$.

**Answer to Question 2:**

```python
# The two calculated equations are lambdified to be used
sol_output = sm.lambdify((y_t_1, s_t_1, pi_t_1, s_t, v, phi, alpha, gamma, h, b), Output[0])
sol_inflation = sm.lambdify((y_t_1, s_t_1, pi_t_1, s_t, v, phi, alpha, gamma, h, b), Inflation[0])

# Set the parameters equal to their values
def _sol_output(y_t_1, s_t_1, pi_t_1, s_t, v, phi=par['phi'], alpha=par['alpha'], gamma=par['gamma'], h=par['h'], b=par['b']):
    return sol_output(y_t_1, s_t_1, pi_t_1, s_t, v, phi, alpha, gamma, h, b)

def _sol_inflation(y_t_1, s_t_1, pi_t_1, s_t, v, phi=par['phi'], alpha=par['alpha'], gamma=par['gamma'], h=par['h'], b=par['b']):
    return sol_inflation(y_t_1, s_t_1, pi_t_1, s_t, v, phi, alpha, gamma, h, b)

# The variables' values are inserted into the functions
A = _sol_output(y_t_1=0, s_t_1=0, pi_t_1=0, s_t=0, v=0)
B = _sol_output(y_t_1=0, s_t_1=0, pi_t_1=0, s_t=0, v=0.1)

C = _sol_inflation(y_t_1=0, s_t_1=0, pi_t_1=0, s_t=0, v=0)
D = _sol_inflation(y_t_1=0, s_t_1=0, pi_t_1=0, s_t=0, v=0.1)

print('The values of y_t and pi_t when all the variables are equal to zero:')
print(f'y_t = {A}')
print(f'pi_t = {C}')
print('The values of y_t and pi_t when all the variables are equal to zero, except v = 0.1')
print(f'y_t = {B}')
print(f'pi_t = {D}')
```

    The values of y_t and pi_t when all the variables are equal to zero:
    y_t = 0.0
    pi_t = 0.0
    The values of y_t and pi_t when all the variables are equal to zero, except v = 0.1
    y_t = 0.0244140625
    pi_t = 0.0018310546875

**Conclusion:** when the values of the parameters are equal to zero ($y_{t-1} = \pi_{t-1} = v_t = s_t = s_{t-1} = 0$), then the equlibrium is $(\pi_t,y_t) = (0,0)$. On the other hand when the economy endurs from a positive demand shock ($v_t = 0.1$ with the rest of the parameters remaining equal to zero), then the equilibrium is $(\pi_t,y_t) = (0.0244140625 , 0.0018310546875)$.

**Note:** to plot the AD-SRAS we need the equations to express the relationship between $\pi_t$ and $y_t$; the lambdified equations don't express directly that relationship, so we will define the original AD and SRAS functions and then plot them.

```python
# Define the AD and SRAS functions to plot them
def AD(h, alpha, v, b, y_t):
    return (1/(h*alpha))*(v - (1+b*alpha)*y_t)

def SRAS(pi_t_1, gamma, y_t, phi, y_t_1, s_t, s_t_1):
    return (pi_t_1 + gamma*y_t - phi*gamma*y_t_1 + s_t - phi*s_t_1)
```

```python
fig = plt.figure(figsize = (12,8))
ax = fig.add_subplot(1,1,1)

y_lin = np.linspace(-0.3, 0.3, 100)
AD_0 = AD(h=par['h'], alpha=par['alpha'], v=0, b=par['b'], y_t=y_lin)
AD_1 = AD(h=par['h'], alpha=par['alpha'], v=0.1, b=par['b'],y_t=y_lin)
SRAS_total = SRAS(pi_t_1=0, gamma=par['gamma'], y_t=y_lin, phi=par['phi'], y_t_1=0, s_t=0, s_t_1=0)

plt.plot(y_lin, AD_0, label='AD 0 (with no disturbance)')
plt.plot(y_lin, AD_1, label='AD 1 (with demand disturbance, v=0.1)')
plt.plot(y_lin, SRAS_total, label='SRAS')
plt.plot(A, C, marker='.', color='black', label='Equilibrium before the demand disturbance')
plt.plot(B, D, marker='o', color='black', label='Equilibrium after the demand disturbance')
plt.grid(True)

plt.title('AD - SRAS')
plt.xlabel('$y_t$')
plt.ylabel('$\pi_t$')
plt.legend(loc='upper right')
plt.show()
```

**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$?

**Answer to Question 3:**

First we will define the functions of the two shocks, which will be used to fill the vectors of $v_{t}$ and $s_{t}$ and the simulate the model to investigate the evolution of the two main variables of our model $(\pi_{t}, y_{t})$

**Note:** don't forget to run the above cell with the parameter values

```python
def v_t(v_t_1, x_t, delta=par['delta']):
    return delta*v_t_1 + x_t

def s_t(s_t_1, c_t, omega=par['omega']):
    return omega*s_t_1 + c_t
```

```python
# Create a random seed
seed = 2019
np.random.seed(seed)

# Define four periods to check which one will present the evolution of the economy better
#T1 = 25
#T2 = 50
#T3 = 100
T4 = 125

# Creating the vectors that will be needed for the simulation
y_vec = [0]
pi_vec = [0]
v_vec = [0]
x_vec = np.zeros(T4) # all the demand shocks are set equal to 0
x_vec[1] = 0.1 # the second element of the demand shocks list is set to 0.1, because demand disturbance function uses v_{t-1}
s_vec = [0]
c_vec = np.zeros(T4) # all the supply shocks are 0
```

```python
# Creating a for loop to fill in the vectors
for t in range (1, T4):
    v_vec.append(v_t (v_vec [t-1], x_vec[t]))
    s_vec.append(s_t (s_vec [t-1], c_vec[t]))
    y_vec.append(sol_output (y_vec[t-1], s_vec[t-1], pi_vec[t-1], s_vec[t], v_vec[t], par['phi'], par['alpha'], par['gamma'], par['h'], par['b']))
    pi_vec.append(sol_inflation (y_vec[t-1], s_vec[t-1], pi_vec[t-1], s_vec[t], v_vec[t], par['phi'], par['alpha'], par['gamma'], par['h'], par['b']))

# Checking the created vectors (uncomment next row to check the values from any of the vectors)
#y_vec, pi_vec, v_vec, s_vec
```

```python
periods = np.linspace(0, T4, T4)

fig = plt.figure(figsize = (10,8))
ax = fig.add_subplot(1,1,1)

ax.plot(periods, pi_vec, label='$\pi_t$')
ax.plot(periods, y_vec, label='$y_t$')
plt.grid(True)

plt.ylim(-0.005, 0.025)
ax.set_title('Evolution of output and inflation')
plt.xlabel('$Periods$')
plt.ylabel('$y_t$ and $\pi_t$')
plt.legend(loc='upper right')
```

**Conclusion:** the two variables ($y_t$, $\pi_t$), that we want to investigate, are affected by a demand shock ($x_t=0.1$) in the interval  from zero (0) to the convergence point. First, we used 25, 50 and 100 periods to check how many periods are needed for our economy to converge back to its equilibrium ($y_t=\pi_t=0$); from the graph we can see that at least 100 periods are required for the convergence. So, the chosen number of periods is 125. From the plot, it is shown that for both variables the demand shock has a bigger effect during the first periods (periods < 20), even though our economy needs more than 100 periods to converge back to its equilibrium.

**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})$

**Answer to Question 4:**

**Note:** now we know that the demand and supply shocks are normally distributed (and that's how they are going to be defined), so we are going to follow the same methodology as before to simulate the AS-AD model for 1000 periods.

```python
# Create a random seed
seed = 2020
np.random.seed(seed)

# Define the periods of the model simulation
T5 = 1000

# Creating the vectors that will be needed for the simulation
y_vec_1 = [0]
pi_vec_1 = [0]
v_vec_1 = [0]
x_vec_1 = np.random.normal(loc=0, scale=par['sigma_x'], size=T5) # all the demand shocks are normally distributed
s_vec_1 = [0]
c_vec_1 = np.random.normal(loc=0, scale=par['sigma_c'], size=T5) # all the supply shocks are normally distributed
```

```python
# Creating a for loop to fill in the vectors
for t in range (1, T5):
    v_vec_1.append(v_t (v_vec_1[t-1], x_vec_1[t]))
    s_vec_1.append(s_t (s_vec_1[t-1], c_vec_1[t]))
    y_vec_1.append(sol_output (y_vec_1[t-1], s_vec_1[t-1], pi_vec_1[t-1], s_vec_1[t], v_vec_1[t], par['phi'], par['alpha'], par['gamma'], par['h'], par['b']))
    pi_vec_1.append(sol_inflation (y_vec_1[t-1], s_vec_1[t-1], pi_vec_1[t-1], s_vec_1[t], v_vec_1[t], par['phi'], par['alpha'], par['gamma'], par['h'], par['b']))
```

```python
periods = np.linspace(0, T5, T5)

fig = plt.figure(figsize = (15,6))
fig.suptitle('Graphical simulation of the model', fontsize=20)

ax1 = fig.add_subplot(2,1,1)
ax1.plot(periods, pi_vec_1, label='$\pi_t$')
ax1.set_title('Evolution of Inflation')
plt.ylabel('$\pi_t$')
plt.grid(True)
plt.legend(loc='upper right')

ax2 = fig.add_subplot(2,1,2)
ax2.plot(periods, y_vec_1, color='orange', label='$y_t$')
ax2.set_title('Evolution of Output')
plt.ylabel('$y_t$')
plt.xlabel('$Periods$')
plt.grid(True)
plt.legend(loc='upper right')
```

```python
# Calculate the requested statistics:
var_y_t = np.var(y_vec_1)
var_pi_t = np.var(pi_vec_1)
corr_y_t_pi_t = np.corrcoef(y_vec_1, pi_vec_1)
corr_y_t_y_t_1 = np.corrcoef(y_vec_1[1:], y_vec_1[:-1])
corr_pi_t_pi_t_1 = np.corrcoef(pi_vec_1[1:], pi_vec_1[:-1])

# Print the statistics
print(f'The variance of the output is: var(y_t) = {var_y_t}')
print(f'The variance of the inflation is: var(pi_t) = {var_pi_t}')
print(f'The correlation between the inflation and the output is: corr(pi_t, y_t) = {corr_y_t_pi_t[0,1]}')
print(f'The auto-correlation between the output (of the current period) and the output (of the previous period) is: corr(y_t,y_t_1) = {corr_y_t_y_t_1[0,1]}')
print(f'The auto-correlation between the inflation (of the current period) and the inflation (of the previous period) is: corr(pi_t, pi_t_1) = {corr_pi_t_pi_t_1[0,1]}')
```

    The variance of the output is: var(y_t) = 1.824925348354874
    The variance of the inflation is: var(pi_t) = 1.1449104934286185
    The correlation between the inflation and the output is: corr(pi_t, y_t) = -0.1314060958224771
    The auto-correlation between the output (of the current period) and the output (of the previous period) is: corr(y_t,y_t_1) = 0.7789670127564
    The auto-correlation between the inflation (of the current period) and the inflation (of the previous period) is: corr(pi_t, pi_t_1) = 0.9785490722526474

**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.

**Answer to Question 5:**

```python
# Create a random seed
seed = 2021
np.random.seed(seed)

# Define the periods of the model simulation
T6 = 1000

# Define the demand and supply shocks as normal distributions
x_vec_2 = np.random.normal(loc=0, scale=par['sigma_x'], size=T6)
c_vec_2 = np.random.normal(loc=0, scale=par['sigma_c'], size=T6)
```

```python
# Create a function that will fill in the vectors and the only function parameter will be phi

def phi_function(phi):

    # Create vectors
    y_vec_2 = [0]
    pi_vec_2 = [0]
    v_vec_2 = [0]
    s_vec_2 = [0]
    corr_y_t_pi_t_vec = [0]

    # In (y_vec_2.append...) and (pi_vec_2.append...) all the parameters are substituted with their values apart from phi,
    # which will take the value that we will give when calling the function.
    for t in range (1, T6):
        v_vec_2.append(v_t (v_vec_2[t-1], x_vec_2[t]))
        s_vec_2.append(s_t (s_vec_2[t-1], c_vec_2[t]))
        y_vec_2.append(sol_output (y_vec_2[t-1], s_vec_2[t-1], pi_vec_2[t-1], s_vec_2[t], v_vec_2[t], phi, par['alpha'], par['gamma'], par['h'], par['b']))
        pi_vec_2.append(sol_inflation (y_vec_2[t-1], s_vec_2[t-1], pi_vec_2[t-1], s_vec_2[t], v_vec_2[t], phi, par['alpha'], par['gamma'], par['h'], par['b']))

    corr_y_t_pi_t_vec = np.corrcoef(y_vec_2, pi_vec_2)[1,0]

    return y_vec_2, pi_vec_2, corr_y_t_pi_t_vec

# Unpack solution
y_vec_2, pi_vec_2, corr_y_t_pi_t_vec = phi_function(par['phi'])
```

```python
# Simulate the model and fill in the correlation vector

phi_vec = np.linspace(1e-8, 9.9999999e-1, T6) # define different values for phi between 0 and 1.
# At the beggining we used 0.00000001 and 0.99999999 as the bound for phi values, then print phi_vec
# and finally used the first and last printed value as bounds

correlations = []

for i in phi_vec:
    y_vec_2, pi_vec_2, corr_y_t_pi_t_vec = phi_function(i)
    correlations.append(corr_y_t_pi_t_vec)
```

```python
# Plot the correlations and the respective phi values

plt.figure(figsize = (12,8))
plt.plot(phi_vec, correlations, label='Correlation ($y_{t},\pi_{t}$)')
plt.axhline(y=0.31, linestyle='--', color='black', xmin=0, label='Correlation ($y_{t},\pi_{t}$) = 0.31')

plt.title('Correlations of ($y_{t},\pi_{t}$) for different values of $\phi$')
plt.ylabel('Correlations ($y_{t},\pi_{t}$)')
plt.xlabel('$\phi$')
plt.ylim(-0.2, 0.5)
plt.grid(True)
plt.legend(loc='upper left')
```

**Conclusion:** from the plot we can see that while the value of $\phi$ increases, so the correlation of ($y_{t},\pi_{t}$) increases, too. Moreover, when $\phi$ $\in$ (0, 0.5] (the right bound is approximately), the values of the correlation lie in the negative interval, while when $\phi$ $\in$ $[0.5, 1)$, the values of the correlation lie in the positive interval. Finally, when the value $\phi$ is approximately slightly greater than 0.9 the corr($y_{t},\pi_{t}$) = 0.31.

In the next cell we are going to use two different optimizers, to estimate the values of $\phi$, so as the $corr(y_{t},\pi_{t}) = 0.31$ approximately.

```python
# Define the function that will be used in the optimizer
obj = lambda phi: (np.corrcoef(phi_function(phi)[0], phi_function(phi)[1])[1,0] - 0.31)

# Use two optimizing methods to estimate and cross-check the result
brentq_result = optimize.brentq(obj, a=0, b=1, full_output=False)
print(f'The value of phi estimated with the brentq numerical optimizer is: brentq_result = {brentq_result}')
bisect_result = optimize.bisect(obj, a=0, b=1, full_output=False)
print(f'The value of phi estimated with the bisect numerical optimizer is: bisect_result = {bisect_result}')
```

    The value of phi estimated with the brentq numerical optimizer is: brentq_result = 0.9277138617387728
    The value of phi estimated with the bisect numerical optimizer is: bisect_result = 0.9277138617380842

**Conclusion:** both optimizers (brentq and bisect methods) returned almost the same results, with the first 12/16 decimals being the same. So, when $\phi = 0.92771$ approximately, the correlation of $(y_{t}, pi_{t})$ comes close to 0.31.

**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$

**Answer to question 6:**

We did not manage to solve question 6, but in the next cells we are going to include the latest version of our try.

```python
# Create a random seed
#seed = 2022
#np.random.seed(seed)

# Define the periods of the model simulation
#T7 = 10 # The number of periods is quite small, because we wanted to reduce the complexity of the "for loop" (that exists in the next lines)
         # during our tests. We would set it equal to 1000 for the simulation when we would fix the code

# Create a function that will fill in the vectors and the function's parameters will be phi, sigma_x, sigma_c

#def US_function(phi, sigma_x, sigma_c):

    # Create vectors
#    y_vec_3 = [0]
#    pi_vec_3 = [0]
#    v_vec_3 = [0]
#    s_vec_3 = [0]
#    x_vec_3 = np.random.normal(loc=0, scale=sigma_x, size=T7)
#    c_vec_3 = np.random.normal(loc=0, scale=sigma_c, size=T7)

    # Create vectors for the five statistics
#    var_y_t_vec = [0]
#    var_pi_t_vec = [0]
#    corr_y_t_pi_t_vec_1 = [0]
#    corr_y_t_y_t_1_vec = [0]
#    corr_pi_t_pi_t_1_vec = [0]

#    for t in range (1, T7):
#        v_vec_3.append(v_t (v_vec_3[t-1], x_vec_3[t]))
#        s_vec_3.append(s_t (s_vec_3[t-1], c_vec_3[t]))
#        y_vec_3.append(sol_output (y_vec_3[t-1], s_vec_3[t-1], pi_vec_3[t-1], s_vec_3[t], v_vec_3[t], phi, par['alpha'], par['gamma'], par['h'], par['b']))
#        pi_vec_3.append(sol_inflation (y_vec_3[t-1], s_vec_3[t-1], pi_vec_3[t-1], s_vec_3[t], v_vec_3[t], phi, par['alpha'], par['gamma'], par['h'], par['b']))

#    var_y_t_vec = np.var(y_vec_3)
#    var_pi_t_vec = np.var(pi_vec_3)
#    corr_y_t_pi_t_vec_1 = np.corrcoef(y_vec_3, pi_vec_3)[1,0]
#    corr_y_t_y_t_1_vec = np.corrcoef(y_vec_3[1:], y_vec_3[:-1])[1,0]
#    corr_pi_t_pi_t_1_vec = np.corrcoef(pi_vec_3[1:], pi_vec_3[:-1])[1,0]

#    return y_vec_3, pi_vec_3, var_y_t_vec, var_pi_t_vec, corr_y_t_pi_t_vec_1, corr_y_t_y_t_1_vec, corr_pi_t_pi_t_1_vec

# Unpack solution
#y_vec_3, pi_vec_3, var_y_t_vec, var_pi_t_vec, corr_y_t_pi_t_vec_1, corr_y_t_y_t_1_vec, corr_pi_t_pi_t_1_vec = US_function(par['phi'], par['sigma_x'], par['sigma_c'])

# Simulate the model and fill in the correlation vector

#phi_vec_1 = np.linspace(1e-8, 9.9999999e-1, T7)
#sigma_x_vec = np.linspace(0, 10, 10)
#sigma_c_vec = np.linspace(0, 10, 10)

#vars_y_t = []
#vars_pi_t = []
#corrs =  []
#autocorrs_y_t = []
#autocorrs_pi_t = []

#for i in phi_vec_1:
#    for j in sigma_x_vec:
#        for k in sigma_c_vec:
#            y_vec_3, pi_vec_3, var_y_t_vec, var_pi_t_vec, corr_y_t_pi_t_vec_1, corr_y_t_y_t_1_vec, corr_pi_t_pi_t_1_vec = US_function(par['phi'], par['sigma_x'], par['sigma_c'])
#            vars_y_t.append(var_y_t_vec)
#            vars_pi_t.append(var_pi_t_vec)
#            corrs.append(corr_y_t_pi_t_vec_1)
#            autocorrs_y_t.append(corr_y_t_y_t_1_vec)
#            autocorrs_pi_t.append(corr_pi_t_pi_t_1_vec)
```

```python
# Define the function that will be used in the optimizer
#obj_1 = lambda phi, sigma_x, sigma_c: ((np.var(US_function(phi, sigma_x, sigma_c)[0], US_function(phi, sigma_x, sigma_c)[1] - 1.64)) &
#                                       (np.var(US_function(phi, sigma_x, sigma_c)[0], US_function(phi, sigma_x, sigma_c)[1] - 0.21)) &
#                                       (np.corrcoef(US_function(phi, sigma_x, sigma_c)[0], US_function(phi, sigma_x, sigma_c)[1])[1,0] - 0.31) &
#                                       (np.corrcoef(US_function(phi, sigma_x, sigma_c)[1:][0], US_function(phi, sigma_x, sigma_c)[:-1][1])[1,0] - 0.84) &
#                                       (np.corrcoef(US_function(phi, sigma_x, sigma_c)[1:][0], US_function(phi, sigma_x, sigma_c)[:-1][1]) - 0.48))

#E = obj_1(phi, sigma_x, sigma_c)

# Use two optimizing methods to cross-check the result
#brentq_result = optimize.brentq(E, a=0, b=1, full_output=False)
#print(f'The value of phi estimated with the brentq numerical optimizer is: brentq_result = {brentq_result}')
#bisect_result = optimize.bisect(obj_1, a=0, b=1, full_output=False)
#print(f'The value of phi estimated with the bisect numerical optimizer is: bisect_result = {bisect_result}')
```

# 3. Exchange economy

Consider an **exchange economy** with

1. 3 goods, $(x_1,x_2,x_3)$
2. $N$ consumers indexed by \\( j \in \{1,2,\dots,N\} \\)
3. Preferences are Cobb-Douglas with log-normally distributed coefficients

    $$ \begin{eqnarray*}
    u^{j}(x_{1},x_{2},x_{3}) &=&
    \left(x_{1}^{\beta_{1}^{j}}x_{2}^{\beta_{2}^{j}}x_{3}^{\beta_{3}^{j}}\right)^{\gamma}\\
     &  & \,\,\,\beta_{i}^{j}=\frac{\alpha_{i}^{j}}{\alpha_{1}^{j}+\alpha_{2}^{j}+\alpha_{3}^{j}} \\
     &  & \,\,\,\boldsymbol{\alpha}^{j}=(\alpha_{1}^{j},\alpha_{2}^{j},\alpha_{3}^{j}) \\
     &  & \,\,\,\log(\boldsymbol{\alpha}^j) \sim \mathcal{N}(\mu,\Sigma) \\
    \end{eqnarray*} $$

4. Endowments are exponentially distributed,

$$
\begin{eqnarray*}
\boldsymbol{e}^{j} &=& (e_{1}^{j},e_{2}^{j},e_{3}^{j}) \\
 &  & e_i^j \sim f, f(z;\zeta) =  1/\zeta \exp(-z/\zeta)
\end{eqnarray*}
$$

Let $p_3 = 1$ be the **numeraire**. The implied **demand functions** are:

$$
\begin{eqnarray*}
x_{i}^{\star j}(p_{1},p_{2},\boldsymbol{e}^{j})&=&\beta^{j}_i\frac{I^j}{p_{i}} \\
\end{eqnarray*}
$$

where consumer $j$'s income is

$$I^j = p_1 e_1^j + p_2 e_2^j +p_3 e_3^j$$

The **parameters** and **random preferences and endowments** are given by:

```python
# a. parameters
N = 50000
mu = np.array([3,2,1])
Sigma = np.array([[0.25, 0, 0], [0, 0.25, 0], [0, 0, 0.25]])
gamma = 0.8
zeta = 1

# b. random draws
seed = 1986
np.random.seed(seed)

# preferences
alphas = np.exp(np.random.multivariate_normal(mu, Sigma, size=N))
betas = alphas/np.reshape(np.sum(alphas,axis=1), (N,1))

# endowments
e1 = np.random.exponential(zeta,size=N)
e2 = np.random.exponential(zeta,size=N)
e3 = np.random.exponential(zeta,size=N)
```

**Question 1:** Plot the histograms of the budget shares for each good across agents.

**Answer to Question 1:**

The budget shares are given by the fraction of demand per income:

$$
\begin{eqnarray*}
\frac{x_{i}^{\star j}(p_{1},p_{2},\boldsymbol{e}^{j})}{I^j}&=&\frac{\beta^{j}_i\frac{I^j}{p_{i}}}{I^j}&=&\frac{\beta^{j}_i}{p_{i}}  \\
\end{eqnarray*}
$$

The histogram of the budget shares can be presented by ploting betas.

```python
# Plot the histogram of the budgets share for each good
fig = plt.figure(dpi=100)
ax = fig.add_subplot(1,1,1)

goods = ['Good 1', 'Good 2', 'Good 3']
ax.hist(betas, bins=50, label=goods)

ax.set_title('The budget shares of three goods')
ax.set_xlabel('betas')
ax.set_ylabel('Consumers')
plt.legend(loc='upper right')
```

**Conclusion:**
As we can see from the histogram, good 1 has the highest budget share. Good 3 has the lowest budgets share, but it gets most of the consumers. The consumers for good1 and goo2 are almost the same.

Consider the **excess demand functions:**

$$ z_i(p_1,p_2) = \sum_{j=1}^N x_{i}^{\star j}(p_{1},p_{2},\boldsymbol{e}^{j}) - e_i^j$$

**Question 2:** Plot the excess demand functions.

**Answer to Question 2:**

Firstly, we need to define the general demand function to calulate the excess demand.

```python
# Check the values of betas
betas
```

    array([[0.53104511, 0.30693723, 0.16201766],
           [0.50866997, 0.4162364 , 0.07509363],
           [0.83317207, 0.07722226, 0.08960567],
           ...,
           [0.63725404, 0.22027742, 0.14246854],
           [0.78205036, 0.11988119, 0.09806844],
           [0.65552751, 0.24728801, 0.09718448]])

We know from the fucntion of betas and alphas that the first column of betas is for good 1, the second column for good 2, and the third column for good 3. So we can define the demand like the functions  below.

```python
# Demand function:

def demand_good_1_fun(betas, p1, p2, e1, e2, e3):
    I = p1*e1 + p2*e2 + e3
    return betas[:,0]*I/p1

def demand_good_2_fun(betas, p1, p2, e1, e2, e3):
    I = p1*e1 + p2*e2 + e3
    return betas[:,1]*I/p2

def demand_good_3_fun(betas, p1, p2, e1, e2, e3):
    I = p1*e1 + p2*e2 + e3
    return betas[:,2]*I
```

Secondly, excess demand function is obtained by demand minus supply.

```python
# Excess demand function:

def excess_demand_good_1_func(betas, p1, p2, e1, e2, e3):
    demand = np.sum(demand_good_1_fun(betas, p1, p2, e1, e2, e3))
    supply = np.sum(e1)
    excess_demand = demand - supply
    return excess_demand

def excess_demand_good_2_func(betas, p1, p2, e1, e2, e3):
    demand = np.sum(demand_good_2_fun(betas, p1, p2, e1, e2, e3))
    supply = np.sum(e2)
    excess_demand = demand - supply
    return excess_demand
```

Thirdly, plot the excess demand function.

```python
# Return coordinate matrices from coordinate vectors
p1_vec = np.linspace(1, 10, 100)
p2_vec = np.linspace(1, 10, 100)
p1_grid, p2_grid = np.meshgrid(p1_vec, p2_vec)
```

```python
# Store the function value in two-dimention lists
excess_1_grid = np.ones((100, 100))
excess_2_grid = np.ones((100, 100))

for i, p1 in enumerate(p1_vec):
    for j, p2 in enumerate(p2_vec):
        excess_1_grid[i,j] = excess_demand_good_1_func(betas, p1, p2, e1, e2, e3)
        excess_2_grid[i,j] = excess_demand_good_2_func(betas, p1, p2, e1, e2, e3)
```

```python
# Plot the excess demand function for three goods
fig = plt.figure(figsize = (10,15))

# Excess demand for good 1
ex1 = fig.add_subplot(2,1,1, projection='3d')
ex1.plot_surface(p1_grid, p2_grid, excess_1_grid)
ex1.invert_xaxis()

ex1.set_xlabel('$p_1$')
ex1.set_ylabel('$p_2$')
ex1.set_zlabel('Excess Demand')
ex1.set_title('Good 1')

# Excess demand for good 2
ex2 = fig.add_subplot(2,1,2, projection='3d')
ex2.plot_surface(p1_grid, p2_grid, excess_2_grid)
ex2.invert_xaxis()

ex2.set_xlabel('$p_1$')
ex2.set_ylabel('$p_2$')
ex2.set_zlabel('Excess Demand')
ex2.set_title('Good 2')

plt.tight_layout()
```

**Quesiton 3:** Find the Walras-equilibrium prices, $(p_1,p_2)$, where both excess demands are (approximately) zero, e.g. by using the following tâtonnement process:

1. Guess on $p_1 > 0$, $p_2 > 0$ and choose tolerance $\epsilon > 0$ and adjustment aggressivity parameter, $\kappa > 0$.
2. Calculate $z_1(p_1,p_2)$ and $z_2(p_1,p_2)$.
3. If $|z_1| < \epsilon$ and $|z_2| < \epsilon$ then stop.
4. Else set $p_1 = p_1 + \kappa \frac{z_1}{N}$ and $p_2 = p_2 + \kappa \frac{z_2}{N}$ and return to step 2.

**Answer to Question 3:**

Firstly, we follow the steps of tâtonnement process in question 3 to create a function for finding the equilibrium prices.

```python
# Create a function to find the equilibrium prices
def find_equilibrium(betas, p1, p2, e1, e2, e3, kappa=0.5, eps=1e-5, maxiter=5000):

    t = 0
    while True:

        # a. step 1: excess demand
        Z1 = excess_demand_good_1_func(betas, p1, p2, e1, e2, e3)
        Z2 = excess_demand_good_2_func(betas, p1, p2, e1, e2, e3)

        # b: step 2: stop?
        if  np.abs(Z1) < eps and np.abs(Z2) < eps or t >= maxiter:
            print(f'{t:3d}: p1 = {p1:12.8f} -> excess demand 1 -> {Z1:14.8f}')
            print(f'{t:3d}: p2 = {p2:12.8f} -> excess demand 2 -> {Z2:14.8f}')
            break

        # c. step 3: update p1
        p1 = p1 + kappa*Z1/N
        p2 = p2 + kappa*Z2/N

        # d. step 4: return
        if t < 5 or t%250 == 0:
            print(f'{t:3d}: p1 = {p1:12.8f} -> excess demand 1 -> {Z1:14.8f}')
            print(f'{t:3d}: p2 = {p2:12.8f} -> excess demand 2 -> {Z2:14.8f}')
        elif t == 5:
            print('   ...')

        t += 1

    return p1, p2
```

Secondly, we initialize the prices and run the tâtonnement process to obtain equilibrium prices.

```python
# Find the equilibrium prices
p1 = 1.5
p2 = 2
kappa = 0.5
eps = 1e-5

p1, p2 = find_equilibrium(betas, p1, p2, e1, e2, e3, kappa=kappa, eps=eps)
```

      0: p1 =   1.96210663 -> excess demand 1 -> 46210.66316021
      0: p2 =   1.79267895 -> excess demand 2 -> -20732.10501031
      1: p1 =   2.23963132 -> excess demand 1 -> 27752.46920159
      1: p2 =   1.63720075 -> excess demand 2 -> -15547.82025340
      2: p1 =   2.43849634 -> excess demand 1 -> 19886.50124673
      2: p2 =   1.52379200 -> excess demand 2 -> -11340.87488112
      3: p1 =   2.59174633 -> excess demand 1 -> 15324.99963530
      3: p2 =   1.44618319 -> excess demand 2 -> -7760.88055883
      4: p1 =   2.71582585 -> excess demand 1 -> 12407.95136982
      4: p2 =   1.39783608 -> excess demand 2 -> -4834.71087112
       ...
    250: p1 =   6.26335411 -> excess demand 1 ->   216.49247363
    250: p2 =   2.53210665 -> excess demand 2 ->    80.79309673
    500: p1 =   6.46787054 -> excess demand 1 ->    20.50967918
    500: p2 =   2.60841773 -> excess demand 2 ->     7.65154831
    750: p1 =   6.48782160 -> excess demand 1 ->     2.05850651
    750: p2 =   2.61586076 -> excess demand 2 ->     0.76794420
    1000: p1 =   6.48982965 -> excess demand 1 ->     0.20775870
    1000: p2 =   2.61660988 -> excess demand 2 ->     0.07750600
    1250: p1 =   6.49003237 -> excess demand 1 ->     0.02098016
    1250: p2 =   2.61668551 -> excess demand 2 ->     0.00782681
    1500: p1 =   6.49005285 -> excess demand 1 ->     0.00211877
    1500: p2 =   2.61669314 -> excess demand 2 ->     0.00079042
    1750: p1 =   6.49005491 -> excess demand 1 ->     0.00021397
    1750: p2 =   2.61669391 -> excess demand 2 ->     0.00007982
    2000: p1 =   6.49005512 -> excess demand 1 ->     0.00002161
    2000: p2 =   2.61669399 -> excess demand 2 ->     0.00000806
    2085: p1 =   6.49005513 -> excess demand 1 ->     0.00000991
    2085: p2 =   2.61669400 -> excess demand 2 ->     0.00000370

Thirdly, ensure that excess demand of both goods are almost zero.

```python
# Ensure that excess demand of both goods are (almost) zero
Z1 = excess_demand_good_1_func(betas, p1, p2, e1, e2, e3)
Z2 = excess_demand_good_2_func(betas, p1, p2, e1, e2, e3)
print(Z1, Z2)
assert(np.abs(Z1) < eps)
assert(np.abs(Z2) < eps)
```

    9.910378139466047e-06 3.6971468944102526e-06

**Conclusion:** by applying the tâtonnement process, we estimated that the Walras-equilibrium prices are $p_1$ = 6.4900 and $p_2$ = 2.6167, where both excess demands are reaching close to zero values (Excess Demand for Good 1 = 9.910e-06 and Excess Demand for Good 2 = 3.697e-06).

**Question 4:** Plot the distribution of utility in the Walras-equilibrium and calculate its mean and variance.

**Answer to Question 4:**

To define the utility function in the Walras-equilibrium, we need to use the equilibrium prices.

```python
# Use the price value in equilibrium
p1 = 6.4900
p2 = 2.6166

# Calculate the utility function
def utility(betas, e1, e2, e3, gamma):
    I = 6.49*e1 + 2.62*e2 + e3
    x1 = betas[:,0]*(I/6.49)
    x2 = betas[:,1]*(I/2.62)
    x3 = betas[:,2]*I
    return (x1**betas[:,0] + x2**betas[:,1] + x3**betas[:,2])**gamma

# Plot the utility function
U = utility(betas,e1, e2, e3, gamma)

plt.hist(U,100)
plt.xlabel('Utility')
plt.ylabel('Consumers')
plt.title('Utilities Distribution In the Walras-equilibrium ')
```

Calculate the mean value and variance:

```python
mean = np.mean(U)
variance = np.var(U)

print(f'mean = {mean:.3f}, variance = {variance:.3f}')
```

    mean = 2.376, variance = 0.208

**Question 5:** Find the Walras-equilibrium prices if instead all endowments were distributed equally. Discuss the implied changes in the distribution of utility. Does the value of $\gamma$ play a role for your conclusions?

**Answer to Question 5:**

Firstly, we make all endowments distributed equally.

```python
# Create equally distributed endowments
e_1 = np.mean(e1) + np.zeros(N)
e_2 = np.mean(e2) + np.zeros(N)
e_3 = np.mean(e3) + np.zeros(N)
```

Secondly, we run the fucntion of tâtonnement process again to find the equilibrium prices in this situation.

```python
# Find the equilibrium prices
p1 = 6
p2 = 2
p1, p2 = find_equilibrium(betas, p1, p2, e_1, e_2, e_3, kappa=kappa, eps=eps)
```

      0: p1 =   5.98161569 -> excess demand 1 -> -1838.43058095
      0: p2 =   2.08252318 -> excess demand 2 ->  8252.31758307
      1: p1 =   5.96812909 -> excess demand 1 -> -1348.65994059
      1: p2 =   2.14602277 -> excess demand 2 ->  6349.95919174
      2: p1 =   5.95841304 -> excess demand 1 ->  -971.60561725
      2: p2 =   2.19591981 -> excess demand 2 ->  4989.70444789
      3: p1 =   5.95164637 -> excess demand 1 ->  -676.66645308
      3: p2 =   2.23573186 -> excess demand 2 ->  3981.20485204
      4: p1 =   5.94721104 -> excess demand 1 ->  -443.53291538
      4: p2 =   2.26787693 -> excess demand 2 ->  3214.50700009
       ...
    250: p1 =   6.42958877 -> excess demand 1 ->    52.50116116
    250: p2 =   2.59617436 -> excess demand 2 ->    19.61473172
    500: p1 =   6.48035834 -> excess demand 1 ->     5.20168564
    500: p2 =   2.61514143 -> excess demand 2 ->     1.94323017
    750: p1 =   6.48542456 -> excess demand 1 ->     0.52273499
    750: p2 =   2.61703404 -> excess demand 2 ->     0.19528030
    1000: p1 =   6.48593405 -> excess demand 1 ->     0.05260559
    1000: p2 =   2.61722437 -> excess demand 2 ->     0.01965208
    1250: p1 =   6.48598532 -> excess demand 1 ->     0.00529473
    1250: p2 =   2.61724353 -> excess demand 2 ->     0.00197797
    1500: p1 =   6.48599048 -> excess demand 1 ->     0.00053292
    1500: p2 =   2.61724546 -> excess demand 2 ->     0.00019909
    1750: p1 =   6.48599100 -> excess demand 1 ->     0.00005364
    1750: p2 =   2.61724565 -> excess demand 2 ->     0.00002004
    1933: p1 =   6.48599105 -> excess demand 1 ->     0.00000999
    1933: p2 =   2.61724567 -> excess demand 2 ->     0.00000373

**Conclusion:** the tâtonnement process show that the equilibrium prices are $p_1$ = 6.4860, $p_2$ = 2.6172. The price for Good 1 is a little bit smaller than before (previous $p_1$ = 6.4900 ), while the price for Good 2 is a little bit larger than before (previous $p_2$ = 2.6167).

In the following part, we will discuss the changes in the distribution of utilty.

Firstly, we need to define the new utility function with equally ditributed endowments.

```python
# New equilibrium prices
p1 = 6.4860
p2 = 2.6172

# Define the new utility function when endowments changed
def utility_1(betas,e_1, e_2, e_3, gamma):
    I = 6.4860*e_1 + 2.6172*e_2 + e_3
    x1 = betas[:,0]*(I/6.4860)
    x2 = betas[:,1]*(I/2.6172)
    x3 = betas[:,2]*(I/1)
    return (x1**betas[:,0] + x2**betas[:,1] + x3**betas[:,2])**gamma

U_1 = utility_1(betas, e_1, e_2, e_3, gamma)
```

Then, we plot the original unitility function and new fucntion in a graph to easily compare the changes by eyes.

```python
# Plot the original and new utility function
plt.hist(U, 100, label='Original Utility Distribution')
plt.hist(U_1, 100, label='New Utility Distribution')
plt.xlabel('Utility')
plt.ylabel('Consumers')
plt.title('Utilities Distribution')
plt.legend()
```

In addition, we calculate the mean value and variance of two utility functions to get the changes in number.

```python
mean_u = np.mean(U)
var_u = np.var(U)

mean_u_1 = np.mean(U_1)
var_u_1 = np.var(U_1)

print(f'original utilty: mean = {mean_u:.3f}, variance = {var_u:.3f}')
print(f'new utilty     : mean = {mean_u_1:.3f}, variance = {var_u_1:.3f}')
```

    original utilty: mean = 2.376, variance = 0.208
    new utilty     : mean = 2.457, variance = 0.004

**Conclusion:**
Obviously, the new unitility function has a higher mean value and a lower variance compared to the original utility function.

Here, we use interactive plot to see if the conclusions change with different value of $\gamma$.

```python
# Plot the original and new utility function in one graph to compare the difference
def interactive(gam):
    uti = utility(betas, e_1, e_2, e_3, gamma=gam)
    uti_1 = utility_1(betas,e_1, e_2, e_3, gamma=gam)

    plt.hist(uti, 500, label='Original Utility Distribution')
    plt.hist(uti_1, 500, label='New Utility Distribution')
    plt.xlabel('Utility')
    plt.ylabel('Consumers')
    plt.title('Utilities Distribution')
    plt.legend()

widgets.interact(interactive,
    gam = widgets.FloatSlider(description='$\\gamma$', min=-1, max=1, step=0.1, value=0))

```

    interactive(children=(FloatSlider(value=0.0, description='$\\gamma$', max=1.0, min=-1.0), Output()), _dom_clas…

    <function __main__.interactive(gam)>

**Conclusion:** from the graph, we know that the mean value changes greatly with different $\gamma$ value. Hence, $\gamma$ does play an important role.


```python
import holoviews as hv
hv.extension('bokeh')
hv.opts.defaults(hv.opts.Curve(width=500),
                 hv.opts.Image(width=500, colorbar=True, cmap='Viridis'))
```

```python
import numpy as np
import scipy.signal
import scipy.fft
from IPython.display import Audio
```

# Análisis de señales no estacionarias utilizando espectrogramas

## ¿Qué ocurre con el espectro de una señal si su frecuencia cambia en el tiempo?

Consideremos este ejemplo sencillo de una señal donde la frecuencia cambia abruptamente en un tiempo determinado

$$
s(t) = \begin{cases}\cos(2\pi f_1 t) & t <0  \\ \cos(2\pi f_2 t) & t \geq 0 \end{cases}
$$

Sea por ejemplo $f_1=440$ Hz y $f_2 = 220$ Hz. Si la graficamos:

```python
f1, f2, Fs = 440, 220, 44100
t = np.arange(-0.5, 0.5, step=1/Fs)
N = len(t)
s = np.concatenate((np.cos(2.0*np.pi*f1*t[:N//2]),
                    np.cos(2.0*np.pi*f2*t[N//2:])))
```

```python
hv.Curve((t, s), 'Tiempo [s]', 'Señal').opts(xlim=(-0.05, 0.05))
```

y si la escuchamos

```python
Audio(s, rate=Fs)
```

Si calculamos la FFT de la señal para obtener su espectro tenemos que

```python
f = scipy.fft.rfftfreq(n=N, d=1./Fs)
S = np.absolute(scipy.fft.rfft(s))
```

```python
hv.Curve((f, S), 'Frecuencia [Hz]', 'Espectro').opts(xlim=(0, 1000))
```

La DFT/FFT nos entrega un "resumen" de todas las frecuencias en la señal

:::{important}

No es posible diferenciar una señal donde ambas frecuencias ocurren al mismo tiempo de otra donde las frecuencias aparecen en tiempos distintos

:::

En general la transformada de Fourier asume que la señal de interés es **estacionaria**. Una señal estacionaria es aquella cuyas propiedades y momentos estadísticos se mantienen en el tiempo

## Frecuencia instantanea

Hasta ahora hemos estudiando señales donde la frecuencia es constante en el tiempo

Definamos la **frecuencia instantanea** como la tasa de cambio del ángulo (fase) en función del tiempo

$$
f(t) =  \frac{1}{2\pi} \frac{d \phi (t)}{dt}
$$

Por ejemplo señal sinusoidal con una frecuencia que cambia en el tiempo sería entonces

$$
s(t) = A \cos( \phi(t) ) = A \cos \left(2\pi \int_0^t f(\tau) d\tau + \phi(0) \right)
$$

de donde podemos notar que si la frecuencia fuera constante, es decir $f(t) = f_0$ $\forall t$, entonces $\int_0^t f(\tau) d\tau = t f_0$ y recuperamos $A\cos(2\pi t f_0 + \phi)$

En cualquier caso donde $f(t)$ no es constante estaremos ante una señal no estacionaria

A continuación dos ejemplos de señales donde la frecuencia cambia con el tiempo

- El chirrido: señal cuya frecuencia cambia entre dos valores
- El vibrato: señal que está modulada en frecuencia por otra señal

### Chirrido o *Chirp*

> Loica (Sturnella loyca). Referencia: http://www.conserva.cl/2009/09/sonidos-de-aves-de-chile-loica.html

Un *chirp* es una señal cuya frecuencia varía suavemente entre un primer valor $f_0$ y un segundo valor $f_1$. Por ejemplo esta variación podría seguir una forma lineal

$$
f(t) = f_{0} + (f_{1} - f_{0}) \frac{(t - t_{0})}{(t_{1} - t_{0})},
$$

donde $t_0$ y $t_1$ son los tiempos en que la señal oscila a $f_0$ y $f_1$, respectivamente. También se puede usar una forma no lineal, por ejemplo cuadrática o exponencial

Los *chirp* se usan como modelo en aplicaciones asociadas a radar y sonar. También se han usado para modelar el canto de algunas aves con el objetivo de hacer identificación automática

Podemos crear un chirrido sintético con `scipy` usando

```python

scipy.signal.chirp(t, # Un vector de tiempos
                   f0, # La frecuencia en el tiempo t=0
                   t1, # El tiempo en el cual f=f1
                   f1, # La frecuencia para el tiempo t=t1
                   method='linear', # Otras opciones disponibles: 'quadratic', 'logarithmic' o 'hyperbolic'
                   ...
                   )

```

```python
f0, f1, Fs = 4000, 2000, 44100
t = np.arange(0, 0.5, step=1./Fs);
s = 0.1*scipy.signal.chirp(t, f0=f0, f1=f1, t1=t[-1], method='quadratic')
```

En este ejemplo la frecuencia cambia cuadraticamente

```python
hv.Curve((t, f0 + (f1 - f0)*(t/t[-1])**2), 'Tiempo [s]', 'Frecuencia [Hz]')
```

El resultado sonoro se muestra a continuación

```python
Audio(s, rate=Fs, normalize=False)
```

### Frecuencia Modulada (FM)

La FM es una tecnología para guardar información en la frecuencia de una onda electromagnética. Es un tipo de **codificación** que se usa mucho en transmisiones de radio.

- La onda electromagnética se llama **señal portadora**. En radio corresponde a una sinusoide con una frecuencia central en el rango de 88 a los 108 [MHz]
- La información se llama **señal modulada**. En radio corresponde tipicamente a una señal de voz o a una canción, es decir que está en el rango de los 20 [Hz] a 20 [kHz] (rango audible humano)

Una señal en el rango audible puede viajar algunos metros. En cambio, si va codificada en la señal portadora puede viajar cerca de 50 km

El siguiente esquema muestra la operación que realiza una estación de radio que transmite señales

La radio que recibe la señal debe realizar el proceso inverso, es decir decodificar la información a partir de la frecuencia de la señal que está recepcionando

Matemáticamente la señal modulada $s_m(t)$ modifica la frecuencia central $f_c$ de la señal portadora como sigue

$$
\begin{align}
s(t) & = A_c \cos \left(2\pi \int_0^t \left(f_c + K s_m(\tau) \right) d\tau \right) \nonumber \\
&= A_c \cos \left(2\pi f_c t + 2\pi K  \int_0^t s_m(\tau) d\tau \right), \nonumber
\end{align}
$$

donde $K$ es el coeficiente de modulación y $s(t)$ es la señal que finalmente viaja por el medio

Cada estación de radio transmite su información $s_m(t)$ usando una frecuencia portadora $f_c$ distinta para no traslaparse

### Vibrato

Un [vibrato](https://es.wikipedia.org/wiki/Vibrato) es un efecto musical que consiste en variar periódicamente el tono de una nota.

Un violinista logra este efecto presionando una cuerda y luego moviendo su dedo de forma regular como muestra la siguiente animación (en cámara lenta)

Podemos considerar el vibrato como un caso particular de modulación de frecuencia. Si consideremos sólo tonos puros podríamos definir $s_m(t) = \cos(2\pi f_m t)$, con lo que nos queda la siguiente señal

$$
s(t) = A_c \cos \left(2\pi f_c t + \frac{K}{f_m} \sin(2\pi f_m t)  \right),
$$

De la expresión tenemos que

- $f_c$ es la frecuencia o tono central
- $f_m$ es la velocidad a la que cambia el tono central
- $K/f_m$ es la amplitud del cambio del tono cnetral

Podemos implementar un vibrato usando

```python
A_c, K, f_c, f_m, Fs = 1, 50, 220, 8, 44100
t = np.arange(0, 2, step=1/Fs)
sm = np.cos(2.0*np.pi*f_m*t)
s = A_c*np.cos(2.0*np.pi*f_c*t + (K/f_m)*np.sin(2.0*np.pi*f_m*t))
```

La frecuencia de la portadora (azul) aumenta con la amplitud de la modulada (roja)

```python
p1 = hv.Curve((t, s), 'Tiempo[s]', 'Señal', label='Portadora').opts(alpha=0.75)
p2 = hv.Curve((t, sm), 'Tiempo[s]', 'Señal', label='Modulada')
(p1 * p2).opts(hv.opts.Curve(xlim=(0, 0.2)))
```

```python
Audio(s, rate=Fs)
```

## Representación en tiempo y frecuencia

Para estudiar una señal cuya frecuencia cambia en el tiempo debemos estudiar la evolución temporal de su espectro. La herramienta más utilizada para esto se llama espectrograma

El **espectrograma** es una representación visual de la energía de la señal distribuida tanto en el tiempo y en la frecuencia. Es decir que es una representación bidimensional.

La siguiente imagen muestra un espectrograma de una señal de habla humana, una señal altamente no estacionaria cuya frecuencia puede presentar cambios bruscos

Notar que:

- El eje horizontal representa tiempo (segundos)
- El eje vertical representa frecuencia (Hz)
- Se usa color para representar la intensidad energética

En la imagen se puede apreciar como el contenido energético cambia su distribución de forma notoria en  los momentos de respiración (breath) y habla (speech). Muchos algoritmos actuales de reconocimiento de habla (por ejemplo redes neuronales artificiales) operan reconociendo patrones a partir del espectrograma.

:::{important}

A diferencia del espectro, el espectrograma nos permite estudiar los cambios de energía "instantáneos" de la señal

:::

### ¿Cómo se obtiene el espectrograma?

Para calcular el espectrograma se utiliza la *short-time Fourier transform* (STFT). Para el caso de una señal discreta la STFT se define como

$$
S[m, k] = \sum_{n} s[n] w[n-m] e^{-j \frac{2\pi}{N} nk}
$$

Notemos como la STFT tanto del tiempo (índice m) como de la frecuencia (índice k)

En la práctica la STFT consiste en

1. multiplicar la señal por una ventana localizada $w[n-m]$
2. calcular la FFT sobre esa ventana

Esto se repite para distintas ventanas como muestra el siguiente diagrama.

En la parte superior de la imagen, la linea azul es la señal y las lineas rojas son las ventanas desplazadas. En la parte inferior se muestra que de cada ventana desplazada se obtiene un espectro. Finalmente el espectrograma consiste en juntar los espectros de amplitud de cada ventana. Notemos que puede haber traslape entre las ventanas

:::{note}

Cuando utilizamos el espectrograma estamos asumiendo que la señal es "localmente" estacionaria. Es decir que es estacionaria dentro de la ventana.

:::

## Espectrograma en Python

Podemos usar la función de `scipy.signal.spectrogram` cuyos parámetros más relevantes son

```python
spectrogram(x, # Señal
            fs=1.0, # Frecuencia de muestreo
            window=('tukey', 0.25), # Tipo de ventana y parámetros de ventana
            nperseg=None, # Ancho de la ventana en número de muestras
            noverlap=None, # Cantidad de traslape, por defecto es 1/8 del largo de ventana
            ...
           )
```

Esta función retorna una tupla con

- Un arreglo con las frecuencias del espectrograma de largo M
- Un arreglo con los tiempos de las ventanas de largo N
- Una matriz de MxN con los valores del espectrograma

Calculemos y visualicemos el espectrograma de la señal de ejemplo que vimos al principio de esta lección

```python
f1, f2, Fs = 440, 220, 44100
t = np.arange(0.0, 1.0, step=1/Fs)
N = len(t)
s = np.concatenate((np.cos(2.0*np.pi*f1*t[:N//2]),
                    np.cos(2.0*np.pi*f2*t[N//2:])))

freqs, times, Sxx = scipy.signal.spectrogram(s, fs=Fs, nperseg=1024)
```

```python
hv.Image((times, freqs, Sxx), kdims=['Tiempo [s]', 'Frecuencia [Hz]']).opts(ylim=(0, 1000))
```

:::{important}

Con el espectrograma podemos visualizar donde (temporalmente) ocurre cada una de las frecuencias de la señal

:::

A continuación profundizaremos en como seleccionar los argumentos del espectrograma utilizando algunos ejemplos

### Argumentos y trade-off del espectrograma

Para calcular el espectrograma debemos seleccionar

- un tipo o función de ventana, por ejemplo Hamming, Tukey o Kaiser
- un ancho de ventana, expresado tipicamente en número de muestras
- un traslape de ventana, expresado tipicamente en número de muestras o en porcentaje de traslape

En la lección de "Fuga espectral" vimos que la resolución frecuencial en el espectro depende fuertemente del número de muestras (ancho) de la ventana y el tipo o función de la ventana. En particular mientras más ancha es la ventana mejor es la resolución en el eje de frecuencia.

Sin embargo si la ventana es demasiado ancha no podremos identificar bien los eventos más breves o rápidos.

:::{warning}

En el espectrograma existe un compromiso (*trade-off*) entre la resolución en el tiempo y la resolución en frecuencia. No se puede mejorar una sin empeorar la otra.

:::

El siguiente esquema ejemplifica esta situación

Más adelante veremos como el largo de la ventana afecta el espectrograma con un ejemplo práctico

### Espectrograma de un vibrato

Implementemos nuevamente el vibrato con frecuencia instantanea

$$
f(t) = f_c + K \cos(2.0\pi f_m t)
$$

```python
A_c, K, f_c, f_m, Fs = 1, 25, 440, 8, 44100
t = np.arange(0, 1, step=1/Fs)
sm = np.cos(2.0*np.pi*f_m*t)
s = A_c*np.cos(2.0*np.pi*f_c*t + (K/f_m)*np.sin(2.0*np.pi*f_m*t))
window = ('kaiser', 6.)
```

A continuación se muestran tres espectrogramas con distinto largo de ventana. La linea roja punteada corresponde $f(t)$ en función de $t$. En todos los espectrogramas se usó una ventana de Kaiser con $\beta=6$

```python
def plot_spectrogram(Nw):
    display(f'Ventana de {1000*Nw/Fs:0.4f} [ms]')
    freqs, times, Sxx = scipy.signal.spectrogram(s, fs=Fs, nperseg=Nw, noverlap=Nw//1.5, window=window)
    formula = hv.Curve((t, f_c + K*sm)).opts(color='r', line_dash='dashed')
    spectrogram = hv.Image((times, freqs, Sxx), kdims=['Tiempo [s]', 'Frecuencia [Hz]']).opts(ylim=(300, 600))
    return spectrogram * formula
```

```python
plot_spectrogram(512)
```

**Ventana muy angosta:** Resolución temporal superior (eje horizontal) pero gran dispersión en la frecuencia (eje vertical)

```python
plot_spectrogram(8192)
```

**Ventana muy ancha:** Resolución frecuencial superior (eje vertical) pero gran dispersión en el tiempo (eje horizontal)

```python
plot_spectrogram(2048)
```

**Compromiso:** La ventana de 46 [ms] parece presentar un mejor compromiso entre resolución temporal y frecuencial.

:::{important}

El mejor compromiso depende de la tasa de cambio temporal y frecuencial de la señal en particular. No existe una receta para escoger el tamaño de ventana. Lo mejor es siempre probar y estudiar los resultados.

:::

### Espectrograma de una señal de voz

Consideremos la siguiente señal de voz humana y su espectrograma

Utilizaremos la librería `librosa` para cargar el archivo de audio en memoria

```python
import librosa

data, Fs = librosa.load("../../data/123.ogg")
time = np.arange(0.0, len(data)/Fs, step=1/Fs)
display(Audio(data, rate=Fs))
hv.Curve((time, data), 'Tiempo [s]', 'Señal')
```

```python
Nw = 2048
freqs, times, Sxx = scipy.signal.spectrogram(data, fs=Fs, window=('kaiser', 6),
                                             nperseg=Nw, noverlap=Nw//1.5)
```

```python
hv.Image((times, freqs, 10*np.log10(Sxx+1e-10)),
            kdims=['Tiempo [s]', 'Frecuencia [Hz]']).opts(ylim=(0, 2000))
```

Si comparamos con la señal de audio que graficamos antes podemos observar que

- Cada vocal tiene un tono y una distribución de energía distintos
- A diferencia de una señal sintética la voz humana es rica en armónicos
- A diferencia de una señal sintética hay ruido blanco que contamina el espectrograma

Notar que no graficamos la energía si no su logaritmo en base diez. Aplicar `log10` es muy usual para visualizar el espectro en señales de habla ya que su energía varía en un rango muy amplio

```python

```


# Time Series - Lecture 2: Lags, Differencing, AR, and MA

The goal of time series is similar to regressions in that we are trying to separate **trends/patterns/model** from **resisduals/noise**. The difference is that time is a useful component in finding trends/patterns.

Today, we will study **how** time variable is used in finding patterns.

## Agenda

1. Lags and Differencing
2. Autoregression (AR)
3. Rolling Window or moving average (MA)

Let us use the same example to explore these concepts:

```python
import pandas as pd
import numpy as np
import matplotlib.pylab as plt
```

```python
df = pd.read_csv('data/multiTimeline.csv', skiprows=1)
df.columns = ['month', 'diet', 'gym', 'finance']
df.month = pd.to_datetime(df.month)
df.set_index('month', inplace=True)
df.plot(figsize=(9,9), linewidth=2, fontsize=15)
```

## Lags and Differencing

### Lags
- Normally, "lag of $X_t$" is denoted $LX_t = X_{t-1}$, $L^2X_t=X_{t-2}$, and etc.
- In data, usually create lags by shifting index by one, and normally you loose one data point

```python
df.diet
```

    month
    2004-01-01    100
    2004-02-01     75
    2004-03-01     72
    2004-04-01     69
    2004-05-01     73
                 ...
    2020-04-01     36
    2020-05-01     43
    2020-06-01     43
    2020-07-01     43
    2020-08-01     43
    Name: diet, Length: 200, dtype: int64

```python
df.diet.shift(-1)
```

    month
    2004-01-01    75.0
    2004-02-01    72.0
    2004-03-01    69.0
    2004-04-01    73.0
    2004-05-01    65.0
                  ...
    2020-04-01    43.0
    2020-05-01    43.0
    2020-06-01    43.0
    2020-07-01    43.0
    2020-08-01     NaN
    Name: diet, Length: 200, dtype: float64

```python
df['diet1'] = df.diet.shift(-1)
```

```python
pd.plotting.lag_plot(df.diet,lag=1)
```

```python
plt.scatter(df['diet'],df['diet1'])
```

### Differencing

- Change between consecutive observations, and can be written as: $$y'_t=y_t-y_{t-1}$$
- Second -order differencing: $$ \begin{align} y''_t & = y'_t-y'_{t-1} \\ & = (y_t-y_{t-1}) - (y_{t-1}-y_{t-2}) \end{align} $$
- Seasonal (or "lag-m differencing"): $$y'_t=y_t-y_{t-m}$$ where $m$ is the period for a season

```python
pd.merge(diet, diet.diff(), left_index=True, right_index=True)
```

      diet_x
      diet_y

      month

      2004-01-01
      100
      NaN

      2004-02-01
      75
      -25.0

      2004-03-01
      72
      -3.0

      2004-04-01
      69
      -3.0

      2004-05-01
      73
      4.0

      ...
      ...
      ...

      2020-04-01
      36
      2.0

      2020-05-01
      43
      7.0

      2020-06-01
      43
      0.0

      2020-07-01
      43
      0.0

      2020-08-01
      43
      0.0

200 rows × 2 columns

```python
pd.merge(diet, diet.diff(periods=2), left_index=True, right_index=True)
```

      diet_x
      diet_y

      month

      2004-01-01
      100
      NaN

      2004-02-01
      75
      NaN

      2004-03-01
      72
      -28.0

      2004-04-01
      69
      -6.0

      2004-05-01
      73
      1.0

      ...
      ...
      ...

      2020-04-01
      36
      -11.0

      2020-05-01
      43
      9.0

      2020-06-01
      43
      7.0

      2020-07-01
      43
      0.0

      2020-08-01
      43
      0.0

200 rows × 2 columns

- Differencing can eliminates (or reduces) trend and seasonality

```python
df['diet'].plot()
```

```python
diet.diff().plot()
```

See that you have removed much of the trend and you can really see the peaks in January every year. Note: You can also perform additional data manipulations (e.g., 2nd order differencing) if the trend is not yet entirely removed. See [here](https://otexts.com/fpp2/stationarity.html) for more on differencing.

Differencing can be helpful in turning your time series into a stationary time series.

> Stationarity: properties of the data generating process do not depend on the time at which the series is observed.

Stationary time series are useful because many time series forecasting methods are based on the assumption that the time series is approximately stationary.

More noticeable example can be found in asset prices. For example, let's look into 200 consecurive days of Google stock price and differenced value.

```python
from PIL import Image
Image.open("images/GOOGdiff.png")
```

## Autoregression

- The model predicts the variable of interest using a linear combination of **historical** values of the variable.
- An **AR(p) model** is written as:
$$y_t = \beta_0 + \beta_1y_{t-1} + \beta_2y_{t-2} + \cdots + \beta_py_{t-p}+\epsilon_t$$

For an AR(1) model, when:

- $\beta_1=0$, the model, or $y_t$ is equivalent to noise
- $\beta_1=1$ and $\beta_0=0$, the model, or $y_t$ is equivalent to random walk
- $\beta_1=1$ and $\beta_0 \neq 0$, the model, or $y_t$ is equivalent to random walk with a drift
- $\beta_1<0$, $y_t$ tends to oscillate around the mean

We normally use autoregressive models for the following stationary cases:
- for an AR(1) model, $-1<\beta_1<1$
- for an AR(2) model, $-1<\beta_2<1,\beta_1+\beta_2<1,\beta_2-\beta_1<1$

when $p\geq3$, the conditions are more complicated.

```python
df.index = pd.DatetimeIndex(df.index).to_period('M')
```

```python
from statsmodels.tsa.arima_model import ARMA
model = ARMA(df.diet, order=(1,0))
result = model.fit()
print(result.summary())
```

                                  ARMA Model Results
    ==============================================================================
    Dep. Variable:                   diet   No. Observations:                  200
    Model:                     ARMA(1, 0)   Log Likelihood                -652.941
    Method:                       css-mle   S.D. of innovations              6.323
    Date:                Sun, 20 Sep 2020   AIC                           1311.882
    Time:                        21:16:41   BIC                           1321.777
    Sample:                    01-31-2004   HQIC                          1315.887
                             - 08-31-2020
    ==============================================================================
                     coef    std err          z      P>|z|      [0.025      0.975]
    ------------------------------------------------------------------------------
    const         50.1298      1.359     36.888      0.000      47.466      52.793
    ar.L1.diet     0.6730      0.063     10.754      0.000       0.550       0.796
                                        Roots
    =============================================================================
                      Real          Imaginary           Modulus         Frequency
    -----------------------------------------------------------------------------
    AR.1            1.4859           +0.0000j            1.4859            0.0000
    -----------------------------------------------------------------------------

```python
result.plot_predict(start='2004-01-01',end='2020-08-01')
plt.show()
```

```python
df.index
```

    PeriodIndex(['2004-01', '2004-02', '2004-03', '2004-04', '2004-05', '2004-06',
                 '2004-07', '2004-08', '2004-09', '2004-10',
                 ...
                 '2019-11', '2019-12', '2020-01', '2020-02', '2020-03', '2020-04',
                 '2020-05', '2020-06', '2020-07', '2020-08'],
                dtype='period[M]', name='month', length=200, freq='M')

## Moving Average (or Rolling Window)
- Rather than using the past values, a moving average model uses past **errors** in a regression.
- An **MA(q) model** is written as: $$y_t=\beta_0+\epsilon_t+\beta_1\epsilon_{t-1}+\cdots+\beta_q\epsilon_{t-q}$$

We normally use moving average models for the following (invertibility) cases:
- for an MA(1) model, $-1<\beta_1<1$
- for an MA(2) model, $-1<\beta_2<1,\beta_1+\beta_2>-1,\beta_1-\beta_2<1$

when $p\geq3$, the conditions are more complicated.

```python
model = ARMA(df.diet, order=(0,7))
result = model.fit()
print(result.summary())
result.plot_predict(start='2004-01-01',end='2020-08-01')
plt.show()
```

## Recap:
- You've learned the concepts of lags, autocorrelation, and differences.
- You've leraned to visualize the relationships for easier interpretation/undertanding.
- You've learned the theory of AR and MA models.
- You've implemented, interpreted, and visualized the AR and MA models in Python.

## Up next:
Deeper look at time series modeling by further learn inaddition to AR and MA model
- ARMA models
- ARIMA models

# Activities

All of the activities below should be done with your chosen data set.

1. Graph lag, ACF, PACF, and difference (play around with different hyperparameters such as lags) to create interpretation/intuition about your data set.
2. Which model do you think will be appropriate to estimate given your data? Briefly talk through your logic.
3. Code AR models (play around with different hypterparameters), and visualize.
4. Code MA models (play around with different hypterparameters), and visuzlize.

## References

The materials presented here are inspired and modified from the following sources:
- https://otexts.com/fpp2/
- https://github.com/CodeOp-tech/tsa-soyhyoj/blob/master/New_Years_Resolutions_Workshop.ipynb
- https://machinelearningmastery.com/gentle-introduction-autocorrelation-partial-autocorrelation/
- https://s3.amazonaws.com/assets.datacamp.com/production/course_4267/slides/chapter3.pdf


```python
# Header starts here.
from sympy.physics.units import *
from sympy import *

# Rounding:
import decimal
from decimal import Decimal as DX
from copy import deepcopy
def iso_round(obj, pv, rounding=decimal.ROUND_HALF_EVEN):
    import sympy
    """
    Rounding acc. to DIN EN ISO 80000-1:2013-08
    place value = Rundestellenwert
    """
    assert pv in set([
        # place value   #  round to:
        1,              #  1
        0.1,            #  1st digit after decimal
        0.01,           #  2nd
        0.001,          #  3rd
        0.0001,         #  4th
        0.00001,        #  5th
        0.000001,       #  6th
        0.0000001,      #  7th
        0.00000001,     #  8th
        0.000000001,    #  9th
        0.0000000001,   # 10th
        ])
    objc = deepcopy(obj)
    try:
        tmp = DX(str(float(objc)))
        objc = tmp.quantize(DX(str(pv)), rounding=rounding)
    except:
        for i in range(len(objc)):
            tmp = DX(str(float(objc[i])))
            objc[i] = tmp.quantize(DX(str(pv)), rounding=rounding)
    return objc

# LateX:
kwargs = {}
kwargs["mat_str"] = "bmatrix"
kwargs["mat_delim"] = ""
# kwargs["symbol_names"] = {FB: "F^{\mathsf B}", }

# Units:
(k, M, G ) = ( 10**3, 10**6, 10**9 )
(mm, cm) = ( m/1000, m/100 )
Newton = kg*m/s**2
Pa     = Newton/m**2
MPa    = M*Pa
GPa    = G*Pa
kN     = k*Newton
deg    = pi/180

half = S(1)/2

# Header ends here.
#
M,l,EI = var("M,l,EI")

sub_list=[
    ( M,   10 *Newton*m              ),
    ( l,    1 *m                     ),
    ( EI, 200*GPa * 2*mm*6*mm**3/ 12 ),
    ]
l2 = l*l
l3 = l*l*l

K = EI/l3
K *= Matrix(
[
[  4*l2 ,  -6*l ,  2*l2 ,   6*l ],
[ -6*l  ,  12   , -6*l  , -12   ],
[  2*l2 ,  -6*l ,  4*l2 ,   6*l ],
[  6*l  , -12   ,  6*l  ,  12   ],
]
)

p2 = var("psi2")
M1,F1,F2 = var("M1,F1,F2")

u = Matrix([0,0,p2,0])
f = Matrix([M1,F1,M,F2])

unks = [p2,M1,F1,F2]

eq = Eq(K*u , f)
sol = solve(eq, unks)

p2 = sol[p2]
M1 = sol[M1]
F1 = sol[F1]
F2 = sol[F2]

pprint("\nM1 / Nm:")
tmp = M1
pprint(tmp)
tmp = tmp.subs(sub_list)
tmp /= Newton*m
tmp = iso_round(tmp,1)
pprint(tmp)

pprint("\nF1 / N:")
tmp = F1
tmp = tmp.subs(sub_list)
tmp /= Newton
tmp = iso_round(tmp,1)
pprint(tmp)

pprint("\nF2 / N:")
tmp = F2
tmp = tmp.subs(sub_list)
tmp /= Newton
tmp = iso_round(tmp,1)
pprint(tmp)

pprint("\nψ₂:")
tmp = p2
pprint(tmp)

pprint("\nψ₂ / rad:")
tmp = p2
tmp = tmp.subs(sub_list)
tmp = iso_round(tmp,1)
pprint(tmp)

# M1 / Nm:
# M
# ─
# 2
# 5
#
# F1 / N:
# -15
#
# F2 / N:
# 15
#
# ψ₂:
# M⋅l
# ────
# 4⋅EI
#
# ψ₂ / rad:
# 12

```


```python
# Header starts here.
from sympy.physics.units import *
from sympy import *

# Rounding:
import decimal
from decimal import Decimal as DX
from copy import deepcopy
def iso_round(obj, pv, rounding=decimal.ROUND_HALF_EVEN):
    import sympy
    """
    Rounding acc. to DIN EN ISO 80000-1:2013-08
    place value = Rundestellenwert
    """
    assert pv in set([
        # place value   #  round to:
        1,              #  1
        0.1,            #  1st digit after decimal
        0.01,           #  2nd
        0.001,          #  3rd
        0.0001,         #  4th
        0.00001,        #  5th
        0.000001,       #  6th
        0.0000001,      #  7th
        0.00000001,     #  8th
        0.000000001,    #  9th
        0.0000000001,   # 10th
        ])
    objc = deepcopy(obj)
    try:
        tmp = DX(str(float(objc)))
        objc = tmp.quantize(DX(str(pv)), rounding=rounding)
    except:
        for i in range(len(objc)):
            tmp = DX(str(float(objc[i])))
            objc[i] = tmp.quantize(DX(str(pv)), rounding=rounding)
    return objc

# LateX:
kwargs = {}
kwargs["mat_str"] = "bmatrix"
kwargs["mat_delim"] = ""
# kwargs["symbol_names"] = {FB: "F^{\mathsf B}", }

# Units:
(k, M, G ) = ( 10**3, 10**6, 10**9 )
(mm, cm) = ( m/1000, m/100 )
Newton = kg*m/s**2
Pa     = Newton/m**2
MPa    = M*Pa
GPa    = G*Pa
kN     = k*Newton
deg    = pi/180

half = S(1)/2

# Header ends here.
#
w, R = var("ω, R")

w1, w2, vA, vB, vC, x = var("ω₁, ω₂, vᴀ, vʙ, vᴄ, x")

eq1 = Eq( vC, 2*w*R )
eq2 = Eq( vA, 4*w*R )
eq3 = Eq( w2, vA/R  )
eq4 = Eq( vB, w2*2*R )
eq5 = Eq(  0, vC - w1*x )
eq6 = Eq( vC, vB - w1*R )

eqns = [eq1, eq2, eq3, eq4, eq5, eq6]
unks = [w1, w2, vA, vB, vC, x]

sol = solve(eqns, unks, dict=True)
pprint(sol[0])

# ⎧                                    R                  ⎫
# ⎨vʙ: 8⋅R⋅ω, vᴀ: 4⋅R⋅ω, vᴄ: 2⋅R⋅ω, x: ─, ω₁: 6⋅ω, ω₂: 4⋅ω⎬
# ⎩                                    3                  ⎭

```


```python
import tensorflow as tf
import tensorflow_quantum as tfq

import cirq
import sympy as sp
import numpy as np

# visualization tools
%matplotlib inline
import matplotlib.pyplot as plt
from cirq.contrib.svg import SVGCircuit
```

## Basic Parameterized Quantum Circuit

```python
a, b = sp.symbols('a b')
```

```python
# Create two qubits
q0, q1 = cirq.GridQubit.rect(1, 2)

# Create circuit on these qubits using params `a` and `b`
circuit = cirq.Circuit(
    cirq.rx(a).on(q0),
    cirq.ry(b).on(q1), cirq.CNOT(control=q0, target=q1)
)

SVGCircuit(circuit)
```

## Circuit Evaluation

```python
# Calculate a state vector with a=0.5, b=-0.5
resolver = cirq.ParamResolver({a: 0.5, b: -0.5})

output_state_vector = cirq.Simulator().simulate(circuit, resolver).final_state
output_state_vector
```

    array([ 0.9387913 +0.j        , -0.23971277+0.j        ,
            0.        +0.06120872j,  0.        -0.23971277j], dtype=complex64)

### Pauli $\hat Z_0$ Measurement on State Vector

```python
z0 = cirq.Z(q0)

qubit_map = {q0: 0, q1: 1}

z0.expectation_from_wavefunction(output_state_vector, qubit_map).real
```

    0.8775825500488281

### Pauli $\frac{1}{2} \hat Z_0 + \hat X_1$ Measurement on State Vector

```python
z0x1 = 0.5 * z0 + cirq.X(q1)

z0x1.expectation_from_wavefunction(output_state_vector, qubit_map).real
```

    -0.04063427448272705

```python
# Rank 1 tensor containing 1 circuit
circuit_tensor = tfq.convert_to_tensor([circuit])

print(circuit_tensor.shape)
print(circuit_tensor.dtype)
```

    (1,)
    <dtype: 'string'>

```python
# Rank 1 tensor containing 2 Pauli operators
pauli_tensor = tfq.convert_to_tensor([z0, z0x1])
pauli_tensor.shape
```

    TensorShape([2])

```python
# batch of values to sub for `a` and `b` parameters
batch_vals = np.array(np.random.uniform(0, 2 * np.pi, (5, 2)), dtype=np.float32)

cirq_results = []
cirq_simulator = cirq.Simulator()

for val in batch_vals:
    resolver = cirq.ParamResolver({a: val[0], b: val[1]})
    final_state = cirq_simulator.simulate(circuit, resolver).final_state
    cirq_results.append(
        [z0.expectation_from_wavefunction(final_state, {
            q0: 0,
            q1: 1
        }).real])

print('cirq batch results: \n {}'.format(np.array(cirq_results)))
```

    cirq batch results:
     [[ 0.92059928]
     [ 0.80442852]
     [-0.53074777]
     [ 0.44015628]
     [ 0.28704041]]

# Hybrid Neural Network

## Controlled Circuit

```python
# Parameters that the classical Nn will feed values into
control_params = sp.symbols('theta_1 theta_2 theta_3')

# Create parameterized circuit
qubit = cirq.GridQubit(0, 0)

model_circuit = cirq.Circuit(
    cirq.rz(control_params[0])(qubit),
    cirq.ry(control_params[1])(qubit),
    cirq.rx(control_params[2])(qubit)
)

SVGCircuit(model_circuit)
```

## Controller

```python
# Classical neural network layers
controller = tf.keras.Sequential([
    tf.keras.layers.Dense(10, activation='elu'),
    tf.keras.layers.Dense(3)
])

controller(tf.constant([[0.0],[1.0]])).numpy()
```

    array([[0.        , 0.        , 0.        ],
           [0.39359298, 0.45154962, 0.19809864]], dtype=float32)

## Controller-Circuit Connection

```python
# Simulated miscalibration that model will learn to correct
circuits_input = tf.keras.Input(shape=(),
                                # The circuit-tensor has dtype `tf.string`
                                dtype=tf.string,
                                name='circuits_input')

# Set qubits' state to `0` or `1` based on command
commands_input = tf.keras.Input(shape=(1,),
                                dtype=tf.dtypes.float32,
                                name='commands_input')
```

Apply operations to above inputs to define the computation:

```python
dense_2 = controller(commands_input)

# TFQ layer for classically controlled circuits
expectation_layer = tfq.layers.ControlledPQC(model_circuit,
                                             # Observe Z
                                             operators = cirq.Z(qubit))
expectation = expectation_layer([circuits_input, dense_2])
```

Package computation as Keras model:

```python
model = tf.keras.Model(inputs=[circuits_input, commands_input],
                       outputs=expectation)
```

```python
tf.keras.utils.plot_model(model, show_shapes=True, dpi=70)
```

```python
# Command input values to the classical NN
commands = np.array([[0], [1]], dtype=np.float32)

# Desired Z expectation value at output of quantum circuit
expected_outputs = np.array([[1], [-1]], dtype=np.float32)
```

```python
# Input circuit that defines random miscalibration the model will learn to correct
random_rotations = np.random.uniform(0, 2 * np.pi, 3)
noisy_preparation = cirq.Circuit(
  cirq.rx(random_rotations[0])(qubit),
  cirq.ry(random_rotations[1])(qubit),
  cirq.rz(random_rotations[2])(qubit)
)
datapoint_circuits = tfq.convert_to_tensor([
  noisy_preparation
] * 2)  # Make two copied of this circuit

# datapoint_circuits.shape # 2 copies of circuit; 1 copy for each datapoint

# Dry run of tfq model
model([datapoint_circuits, commands]).numpy()
```

    array([[-0.45780095],
           [-0.2368703 ]], dtype=float32)

```python
# Run training process to adjust output values towards `expected_outputs`
optimizer = tf.keras.optimizers.Adam(learning_rate=0.05)
loss = tf.keras.losses.MeanSquaredError()
model.compile(optimizer=optimizer, loss=loss)
history = model.fit(x=[datapoint_circuits, commands],
                    y=expected_outputs,
                    epochs=30,
                    verbose=0)

# Plot training information
plt.plot(history.history['loss'])
plt.title("Learning to Control a Qubit")
plt.xlabel("Iterations")
plt.ylabel("Error in Control")
plt.show()
```

## Verify Outputs

Use the trained model to correct the qubit calibration errors.

```python
def check_error(command_values, desired_values):
  """Based on the value in `command_value` see how well you could prepare
  the full circuit to have `desired_value` when taking expectation w.r.t. Z."""
  params_to_prepare_output = controller(command_values).numpy()
  full_circuit = noisy_preparation + model_circuit

  # Test how well you can prepare a state to get the expectation
  # value in `desired_values`
  for index in [0, 1]:
    state = cirq_simulator.simulate(
        full_circuit,
        {s:v for (s,v) in zip(control_params, params_to_prepare_output[index])}
    ).final_state
    expectation = z0.expectation_from_wavefunction(state, {qubit: 0}).real
    print(f'For a desired output (expectation) of {desired_values[index]} with'
          f' noisy preparation, the controller\nnetwork found the following '
          f'values for theta: {params_to_prepare_output[index]}\nWhich gives an'
          f' actual expectation of: {expectation}\n')

check_error(commands, expected_outputs)
```

    For a desired output (expectation) of [1.] with noisy preparation, the controller
    network found the following values for theta: [ 0.75017124  0.4912489  -1.800404  ]
    Which gives an actual expectation of: 0.9485020637512207

    For a desired output (expectation) of [-1.] with noisy preparation, the controller
    network found the following values for theta: [ 1.7805516   1.2925966  -0.12267798]
    Which gives an actual expectation of: -0.9495996832847595

# Eigenstates of Different Operators

```python
# Define inputs
commands_input = tf.keras.layers.Input(shape=(1),
                                       dtype=tf.dtypes.float32,
                                       name='commands_input')
circuits_input = tf.keras.Input(shape=(),
                                # The circuit-tensor has dtype `tf.string`
                                dtype=tf.dtypes.string,
                                name='circuits_input')
operators_input = tf.keras.Input(shape=(1,),
                                 dtype=tf.dtypes.string,
                                 name='operators_input')

```

```python
# Define classical NN
controller = tf.keras.Sequential([
    tf.keras.layers.Dense(10, activation='elu'),
    tf.keras.layers.Dense(3)
])
```

```python
dense_2 = controller(commands_input)

# append model circuit onto datapoint circuit tensor manually, since PQC or ControlledPQC aren't used
full_circuit = tfq.layers.AddCircuit()(circuits_input, append=model_circuit)
expectation_output = tfq.layers.Expectation()(full_circuit,
                                              symbol_names=control_params,
                                              symbol_values=dense_2,
                                              operators=operators_input)

# Contruct Keras model
two_axis_control_model = tf.keras.Model(
    inputs=[circuits_input, commands_input, operators_input],
    outputs=[expectation_output])

```

```python
# Operators to measure for each command
operator_data = tfq.convert_to_tensor([[cirq.X(qubit)], [cirq.Z(qubit)]])

# Command input values to the classical NN
commands = np.array([[0], [1]], dtype=np.float32)

# Desired expectation value at output of quantum circuit
expected_outputs = np.array([[1], [-1]], dtype=np.float32)

```

## Training

```python
optimizer = tf.keras.optimizers.Adam(learning_rate=0.05)
loss = tf.keras.losses.MeanSquaredError()

two_axis_control_model.compile(optimizer=optimizer, loss=loss)

history = two_axis_control_model.fit(
    x=[datapoint_circuits, commands, operator_data],
    y=expected_outputs,
    epochs=30,
    verbose=1)

```

    Train on 2 samples
    Epoch 1/30
    2/2 [==============================] - 0s 216ms/sample - loss: 2.2954
    Epoch 2/30
    2/2 [==============================] - 0s 859us/sample - loss: 1.3000
    Epoch 3/30
    2/2 [==============================] - 0s 1ms/sample - loss: 0.6678
    Epoch 4/30
    2/2 [==============================] - 0s 1ms/sample - loss: 0.3538
    Epoch 5/30
    2/2 [==============================] - 0s 2ms/sample - loss: 0.1962
    Epoch 6/30
    2/2 [==============================] - 0s 975us/sample - loss: 0.1419
    Epoch 7/30
    2/2 [==============================] - 0s 2ms/sample - loss: 0.1468
    Epoch 8/30
    2/2 [==============================] - 0s 1ms/sample - loss: 0.1486
    Epoch 9/30
    2/2 [==============================] - 0s 780us/sample - loss: 0.1222
    Epoch 10/30
    2/2 [==============================] - 0s 1ms/sample - loss: 0.0795
    Epoch 11/30
    2/2 [==============================] - 0s 756us/sample - loss: 0.0427
    Epoch 12/30
    2/2 [==============================] - 0s 680us/sample - loss: 0.0219
    Epoch 13/30
    2/2 [==============================] - 0s 720us/sample - loss: 0.0136
    Epoch 14/30
    2/2 [==============================] - 0s 1ms/sample - loss: 0.0120
    Epoch 15/30
    2/2 [==============================] - 0s 954us/sample - loss: 0.0141
    Epoch 16/30
    2/2 [==============================] - 0s 1ms/sample - loss: 0.0177
    Epoch 17/30
    2/2 [==============================] - 0s 1ms/sample - loss: 0.0202
    Epoch 18/30
    2/2 [==============================] - 0s 1ms/sample - loss: 0.0192
    Epoch 19/30
    2/2 [==============================] - 0s 838us/sample - loss: 0.0147
    Epoch 20/30
    2/2 [==============================] - 0s 745us/sample - loss: 0.0091
    Epoch 21/30
    2/2 [==============================] - 0s 618us/sample - loss: 0.0047
    Epoch 22/30
    2/2 [==============================] - 0s 600us/sample - loss: 0.0020
    Epoch 23/30
    2/2 [==============================] - 0s 595us/sample - loss: 7.6339e-04
    Epoch 24/30
    2/2 [==============================] - 0s 563us/sample - loss: 3.0771e-04
    Epoch 25/30
    2/2 [==============================] - 0s 797us/sample - loss: 1.8658e-04
    Epoch 26/30
    2/2 [==============================] - 0s 774us/sample - loss: 2.2018e-04
    Epoch 27/30
    2/2 [==============================] - 0s 867us/sample - loss: 3.9720e-04
    Epoch 28/30
    2/2 [==============================] - 0s 621us/sample - loss: 7.7406e-04
    Epoch 29/30
    2/2 [==============================] - 0s 703us/sample - loss: 0.0014
    Epoch 30/30
    2/2 [==============================] - 0s 607us/sample - loss: 0.0022

```python
# Plot training results
plt.plot(history.history['loss'])
plt.title("Learning to Control a Qubit")
plt.xlabel("Iterations")
plt.ylabel("Error in Control")
plt.show()
```

```python
controller.predict(np.array([0,1]))
```

    array([[ 2.0547266 , -0.06274474,  0.22524181],
           [ 0.17548007, -0.6186218 ,  1.0181975 ]], dtype=float32)


# Project 3: Percolation - FYS4460
Author: Øyvind Sigmundson Schøyen

In this project we'll explore _percolation_ from the project shown here: https://www.uio.no/studier/emner/matnat/fys/FYS4460/v19/notes/project2017-ob3.pdf

```python
import numpy as np
import matplotlib.pyplot as plt
import scipy.ndimage as spi
import skimage
import tqdm
import sklearn.linear_model
import seaborn as sns

sns.set(color_codes=True)
```

## Computing the density of spanning clusters

We call $P(p, L)$ the _density of spanning clusters._ It is defined as the probability for a site to belong to a _spanning cluster._ A spanning cluster is defined as a cluster having an extent over the entire system size along one axis. We compute the density by extracting all spanning clusters from a system and then counting all the set sites in the spanning clusters. Dividing the number of set sites contained in all the spanning clusters by the total number of sites we find $P(p, L)$.

```python
def compute_density_of_spanning_clusters(system):
    num_rows, num_cols = system.shape
    total_mass = num_rows * num_cols

    # Label and count the number of connected clusters
    labels, num_features = spi.measurements.label(system)
    # Collect regions
    props = skimage.measure.regionprops(labels)
    num_percolating = 0
    mass = 0

    # Iterate through regions and check if they span the entire system
    for prop in props:
        min_row, min_col, max_row, max_col = prop.bbox

        if max_row - min_row == num_rows or max_col - min_col == num_cols:
            num_percolating += 1
            mass += prop.area

    return mass / total_mass, num_percolating
```

The function `compute_density_of_spanning_clusters` takes in a system and computes the density of spanning clusters, $P(p, L)$. It is general in the sense that it does not distinguish between quadratic or rectangular systems. Below we plot $P(p, L)$ for different $L$ and $p$.

```python
num_systems = 101
p_arr = np.linspace(0, 1, num_systems)
L_list = [50, 100, 200, 500]

plt.figure(figsize=(14, 10))

for L in L_list:
    density_arr = np.zeros_like(p_arr)
    pi_arr = np.zeros_like(p_arr)
    num_rows = L
    num_cols = L

    num_percolating_systems = 0

    for i in tqdm.tqdm_notebook(range(num_systems)):
        p = p_arr[i]
        system = np.random.choice([0, 1], size=(num_rows, num_cols), p=[1 - p, p])
        density_arr[i], num_percolating = compute_density_of_spanning_clusters(system)
        num_percolating_systems += num_percolating > 0

    plt.plot(p_arr, density_arr, label=fr"$L = {L}$")

plt.legend(loc="best")
plt.xlabel(r"$p$")
plt.ylabel(r"$P(p, L)$")
plt.title(r"Density of spanning clusters for varying system sizes $L$")
plt.show()
```

Here we can see the expected behaviour of the density of spanning clusters as a function of the probability $p$. At $p = p_c \approx 0.59$ the density increases almost step-wise before taking on a linear shape.

## Compute density of spanning clusters and spanning probability for a given system size $L$

We now want to create a function which can compute the density of spanning clusters $P(p, L)$ and the spanning probability $\Pi(p, L)$ for a given system size $L$. A way we can do this is by generating a system of size $L$. We can then vary the probability $p$ over a set range and mask the different sites. This lets us compute the density of spanning clusters and the spanning probability efficiently on a given system.

```python
def compute_density_and_probability(system, p_arr):

    num_rows, num_cols = system.shape
    total_mass = num_rows * num_cols
    percolating = np.zeros(*p_arr.shape)
    mass = np.zeros(*p_arr.shape)

    for i, p in enumerate(p_arr):
        p_system = system < p

        # Label and count the number of connected clusters
        labels, num_features = spi.measurements.label(p_system)
        # Collect regions
        props = skimage.measure.regionprops(labels)

        # Iterate through regions and check if they span the entire system
        for prop in props:
            min_row, min_col, max_row, max_col = prop.bbox

            if max_row - min_row == num_rows or max_col - min_col == num_cols:
                percolating[i] += 1
                mass[i] += prop.area

    return mass / total_mass, percolating > 0
```

We now run $10$ experiments, that is regenerate a specific system $10$ times for every system, and find the percolation probability and density of the spanning clusters for systems with $L = 2, 4, 8, 16, 32, 64, 128, 256$.

```python
p_arr = np.linspace(0, 1, 51)
num_experiments = 20

L_list = [2 ** (i + 1) for i in range(8)]
P_list = []
Pi_list = []
```

```python
for L in L_list:
    P_mat = np.zeros((num_experiments, *p_arr.shape))
    Pi_mat = np.zeros_like(P_mat)

    for n in tqdm.tqdm_notebook(range(num_experiments)):
        system = np.random.random((L, L))
        P_mat[n], Pi_mat[n] = compute_density_and_probability(system, p_arr)

    P_list.append(np.average(P_mat, axis=0))
    Pi_list.append(np.average(Pi_mat, axis=0))
```

```python
plt.figure(figsize=(14, 10))
for L, P in zip(L_list, P_list):
    plt.plot(p_arr, P, label=fr"$L = {L}$")

plt.xlabel(r"$p$")
plt.ylabel(r"$P(p, L)$")
plt.legend(loc="best")
plt.title(r"Density of spanning clusters for varying system sizes $L$")
plt.show()
```

Here we can see the density of spanning clusters for $L = 2, 4, 8, 16, 32, 64, 128, 256$. The trend shows that we start getting percolation around $p = p_c \approx 0.57$. We also see the characteristic logarithmic behaviour before density becomes more or less linear up to $p = 1$. We clearly see how an increase in system size makes the density exhibit more of expected behaviour.

```python
plt.figure(figsize=(14, 10))
for L, Pi in zip(L_list, Pi_list):
    plt.plot(p_arr, Pi, label=fr"$L = {L}$")

plt.legend(loc="best")
plt.xlabel(r"$p$")
plt.ylabel(r"$\Pi(p, L)$")
plt.title(r"Percolation probability for varying system sizes $L$")
plt.show()
```

This plot shows the percolation probability $\Pi(p, L)$ for $L = 2, 4, 8, 16, 32, 64, 128, 256$. We see much of the same behaviour as for the density of spanning clusters, that is, that $p_c \approx 0.57$, as this is where the largest system starts to percolate. Here we expect to see a step-function at $p = p_c$, and we can see that as the system increase, this behaviour is approximated well.

## Form of density of spanning cluster when $p > p_c$

We know that when $p > p_c$ we get
\begin{align}
    P(p, L) \propto (p - p_c)^{\beta}.
\end{align}
To find a value for $\beta$, we take the logarithm on both sides yielding
\begin{align}
    \log\Bigl[P(p, L)\Bigr] \propto \beta \log(p - p_c).
\end{align}
We can thus compute $\beta$ as the slope of the curve. For the approximate value of the critical percolation probability, we set $p_c = 0.59275$.

```python
p_c = 0.59275 # Critical percolation probability
p_arr = np.linspace(p_c, 1, 101 + 1)[1:] # p > p_c

log_ppc = np.log(p_arr - p_c) # Dependent variable
```

```python
P_list = []
Pi_list = []

for L in L_list:
    P_mat = np.zeros((num_experiments, *p_arr.shape))
    Pi_mat = np.zeros_like(P_mat)

    for n in tqdm.tqdm_notebook(range(num_experiments)):
        system = np.random.random((L, L))
        P_mat[n], Pi_mat[n] = compute_density_and_probability(system, p_arr)

    P_list.append(np.average(P_mat, axis=0))
    Pi_list.append(np.average(Pi_mat, axis=0))
```

```python
plt.figure(figsize=(14, 10))
for L, P in zip(L_list[-3:], P_list[-3:]):
    log_P = np.log(P)

    plt.plot(
        log_ppc,
        log_P,
        label=fr"L = {L}",
    )

    clf = sklearn.linear_model.LinearRegression(
        fit_intercept=True
    ).fit(log_ppc[:, None], log_P[:, None])

    beta = clf.coef_[0, 0]

    print(f"For L = {L}: beta = {beta}")

    plt.plot(
        log_ppc,
        clf.predict(log_ppc[:, None]).ravel(),
        "--",
        label=fr"Predicted line: L = {L}",
    )

plt.legend(loc="best")
plt.xlabel(r"$\log(p - p_c)$")
plt.ylabel(r"$\log[P(p, L)]$")
plt.title(r"Plot of line fit to compute $\beta$ when $p > p_c$ from density")
plt.show()
```

Here we can see log plots of the functional relation between the density of spanning clusters and the difference between the percolation probability and the critical probability when $p > p_c$. From the largest system size we find $\beta \approx 0.245$.