# Exercise Session 2: Convolution, Filtering and Gradients

The goal of this exercise is to:
* Understand and apply a convolutional filter to an image
* Compare the computational complexity of separable and non-separable filters
* Compute image gradients and apply them to real-world images

```python
%matplotlib inline

import numpy as np
import matplotlib.pyplot as plt
import cv2
import time
import scipy.signal as conv

plt.rcParams['figure.figsize'] = (10, 10)
plt.rcParams['image.cmap'] = 'gray'
```

## Exercise 1: Applying Convolutional Filters

Assume we are given a gray-scale image $I[x, y]$, of size $W \times H$, such that $0 \leq x \leq W-1$,
and $0 \leq y \leq H-1$. We want to apply a filter $F[i, j]$ to image $I$. The filter $F$ is of size $(2N + 1) \times (2M + 1)$, such that $−N \leq i \leq N$, and $−M \leq j \leq M$.

The result can be computed as

\begin{align}
R[x, y] = (I ∗ F)[x, y] = \sum_{i=-N}^{N} \sum_{i=-M}^{M} I[x − i, y − j]~F[i, j]
\end{align}

* Implement a function ```R = applyImageFilter(I,F)``` that takes a
gray-scale image $I$ and a filter $F$ as inputs, and returns the result of the convolution
of the two.
Note: There are many python libraries providing convolution function to convolve an image with a filter,
but in this exercise you are requested to implement your own. This will help your understanding
of how convolutional filters work.
    -  To avoid numerical issues, make sure $I$ and $F$ are of type float. To understand
why, think what would happen if you add two unsigned 8-bit numbers when
computing the convolution, for example 240 and 80.
    -  What happens when computing $R[x, y]$ near the border of the image? What
would you propose to deal with this issue?

```python
def applyImageFilter(I, F):
    # First input parameter: I
    #     Input image. It should be a 2D matrix. According to the notation in the description, it has W rows and
    #     H columns.
    # Second input parameter: F
    #     Filter used for the convolution. It should be a 2D matrix. According to the notation in the decription,
    #     it has (2N+1) rows and (2M+1) columns.

    # If image is not of type float, convert it to float
    if not np.issubdtype(I.dtype,float):
        I = np.float64(I)

    # If the filter is not of type float, convert it to float
    if not np.issubdtype(F.dtype,float):
        F = np.float64(F)

    # Shape of Filter
    N_, M_ = F.shape

    # Check whether the dimensions of input are accurate, they should be odd
    if not N_%2:
        raise ValueError('Number of rows in the filter must be odd')

    if not M_%2:
        raise ValueError('Number of columns in the filter must be odd')

    # Compute the values for N,M which is used in the above description.
    N = np.int64((N_-1)/2)
    M = np.int64((M_-1)/2)

    # Shape of the input image
    W, H = I.shape

    # ******************************************
    # Initialize output matrix R
    # ******************************************
    ### your code here
    # R =
    # after implemntation remove the line below
    raise ValueError('Not implemented yet.')
    # ******************************************

    # Output image size
    W_R, H_R = R.shape

    # ******************************************
    # Perform the convolution here
    # Hint 1: You can implement it with 4 nested for loops
    # Hint 2: Outer two for loops should iterate over the pixels in output image
    # Hint 3: Inner two for loops are used to compute the R[x,y] value
    # ******************************************
    ### your code here
    # after implemntation remove the line below
    raise ValueError('Not implemented yet.')
    # ******************************************

    return R
```

```python
# Below code is to verify your implementation of the convolution
# we compare your output with the scipy.signal implementation of the convolution
# and the error should be zero

# Read the sample image
img = cv2.imread('images/res1.png',0)

# Generate a random filter F of size 3 x 3
F = np.random.randn(5,3)

# Perform convolution with the function you implemented
output_1 = applyImageFilter(img, F, padding='same')

# Perform convolution with scipy.signal implementation of convolution
output_2 = conv.convolve2d(img, F , mode='same')

# Error
print(f'Input shape: {img.shape}')
print(f'Output shape: {output_1.shape}')
print(f'Error: {np.sum(np.abs(output_1 - output_2))}')
# if the implmentation is accurate, the error will be 0
```

## Exercise 2: Image Smoothing

Convolutional filters have many uses. A very common one is to smooth or soften an
image. A typical smoothing filter is the Gaussian Filter, which follows the equation of a
2D Gaussian probability distribution. It is an important preprocessing step typically to reduce the amount of noise.  When constructing a Gaussian filter we can specify the size and standard deviation (σ) of the underlying Gaussian function.

* To create a Gaussian filter use the function given below.

```python
def gaussian_filter(fSize, fSigma):
    x, y = np.mgrid[-fSize//2 + 1:fSize//2 + 1, -fSize//2 + 1:fSize//2 + 1]
    g = np.exp(-((x**2 + y**2)/(2.0*fSigma**2)))
    return g/g.sum()
```

* What are fSize and fSigma?
* How do fSize and fSigma affect the filter’s shape and size?
* Visualize different filters by giving different values to these parameters.
* If you are given fSigma, how would you choose fSize?

```python

```

* Apply Gaussian filtering to 'res1.png'
* To apply the filter to an image use ```cv2.GaussianBlur(img, fSize, fSigma)```
* Experiment with different values of fSigma. How does the amount of smoothing vary with this parameter?

```python
img = cv2.imread('images/res1.png',0)
plt.imshow(img)

# apply filters, try different values of Sigma
### your code here

```

As you saw in the first exercise of this session, applying the filter near the border of
the image is not a well-defined problem. It is possible to deal with this issue through
different approaches. Luckily, cv2.GaussianBlur implements different ways of dealing with border effects.
Here you will try them out and understand what each of them does.

*  Apply a Gaussian filter with fSigma = 10 and fSize = 25 to 'res1.png'
* Use borderType flag of cv2.GaussianBlur function to define what kind of border should be added around the image (cv2.BORDER_CONSTANT, cv2.BORDER_REFLECT, cv2.BORDER_REPLICATE).

```python
img = cv2.imread('images/res1.png',0)
# apply different border types for blur filters
### your code here

```

## Exercise 3: Edge detector

Edge detection is one of the most important operations in Computer Vision. In this exercise we will investigate how it
can be performed and what information can be obtained from it.

One of the ways to detect edges is to compute image intensity gradients. They can be approximated by convolving image with Sobel filters. They consist of 2 $3 \times 3$ filters:

\begin{equation*}
S_x = \frac{1}{8}
\begin{bmatrix}
-1 & 0 & +1\\
-2 & 0 & +2\\
-1 & 0 & +1\\
\end{bmatrix}
\hspace{2cm}
S_y = \frac{1}{8}
\begin{bmatrix}
-1 & -2 & -1\\
0 & 0 & 0\\
+1 & +2 & +1\\
\end{bmatrix}
\end{equation*}

where $S_x$ computes the partial derivative of the image in the horizontal direction, while $S_y$ does it in the vertical direction.

* Compute the image derivatives in x- and y-directions using Sobel filters. You can use for that function ```python cv2.Sobel()```

```python
img_coins = cv2.imread('images/coins.png',0)

### Compute gradient in x-direction
### your code here
# grad_x =

### Compute gradient in y-direction
### your code here
# grad_y =
```

* Visualize the image gradients. How can you explain the differences between the two results?

```python
fig = plt.figure()
plt.subplot(1,2,1)
grady = plt.imshow(grad_x,cmap="jet")
plt.title("Gradient x")
plt.colorbar(fraction=0.046, pad=0.04)
plt.subplot(1,2,2)
gradx = plt.imshow(grad_y,cmap="jet")
plt.title("Gradient y")
plt.colorbar(fraction=0.046, pad=0.04)
plt.tight_layout()
```

Mathematically, the goal of the filters $S_x$ and $S_y$ is to approximate the derivatives of the image with respect to
the horizontal and vertical directions respectively, such that
\begin{equation*}
\nabla I_x(x,y) = (I*S_x)[x,y]
\quad\mathrm{and}\quad
\nabla I_y(x,y) = (I*S_y)[x,y]
\end{equation*}

Therefore, the gradient of an image at each point is a 2D vector
\begin{equation*}
\nabla I =
\begin{bmatrix}
\nabla I_x\\
\nabla I_y
\end{bmatrix}
\end{equation*}

This vector can be computed for every pixel. Its magnitude and phase can be computed as
\begin{equation*}
|| \nabla I || = \sqrt{(\nabla I_x)^2 + (\nabla I_y)^2} \\
\angle \nabla I = atan2(\nabla I_x, \nabla I_y)
\end{equation*}

$\it{Note:}$ we use atan2() instead of atan() to be able to determine the right quadrant of the phase.

* Using the previously computed image gradients and the above formula compute the gradient magnitude and gradient phase.

```python
### Compute gradient magnitude
### your code here
# grad_mag =

### Compute gradient phase
### your code here
# grad_phase =
```

* Visualize the gradient magnitude and phase images along with the original image. Where does the gradient have high
magnitude? How does the phase change along the coin border?

```python
fig = plt.figure()
plt.subplot(1,3,1)
plt.imshow(img_coins)
plt.subplot(1,3,2)
grady = plt.imshow(grad_mag,cmap="jet")
plt.title("Gradient magnitude")
plt.colorbar(fraction=0.046, pad=0.04)
plt.subplot(1,3,3)
gradx = plt.imshow(grad_phase)
plt.title("Gradient phase")
plt.colorbar(fraction=0.046, pad=0.04)
plt.tight_layout()
```

The gradient has got the highest magnitude at the borders of the coins. For that reason it can be considered a good
proxy for detecting edges. The phase of the gradient changes smoothly along the circular coin border along with the
normal of the boundary.

## Exercise 4: Separable Filters

As seen in class, certain types of 2D filters can be thought of as the composition of two
1-dimensional filters. These are called Separable Filters, and can be computed more
efficiently than those who are non-separable.

For example, the Sobel filter $S_x$ can be decomposed as

\begin{equation*}
\begin{bmatrix}
-1 & 0 & +1\\
-2 & 0 & +2\\
-1 & 0 & +1\\
\end{bmatrix} =
\begin{bmatrix}
+1 \\
+2 \\
+1 \\
\end{bmatrix}
\begin{bmatrix}
-1 & 0 & +1
\end{bmatrix}
\end{equation*}

which means that the convolution of I with the 2D filter $S_x$ can be simplified to two
convolutions with 1D filters,

\begin{equation*}
I * S_x =
\frac{1}{8} I *
\begin{bmatrix}
-1 & 0 & +1\\
-2 & 0 & +2\\
-1 & 0 & +1\\
\end{bmatrix}
\end{equation*}

\begin{equation*}
=
\frac{1}{8}\Bigg(I *
\begin{bmatrix}
+1\\
+2\\
+1\\
\end{bmatrix} \Bigg) *
\begin{bmatrix}
-1 & 0 & +1
\end{bmatrix}
\end{equation*}

For the comparison of the computational complexity of separable and non-separable
filters, check out the lecture notes.

* We will compare the performance of separable and non-separable filters.
    -  Create a separable filter (for example a Gaussian filter, you can use  ```gaussian_filter``` function given below)
    -  Use ```decomposeSeparableFilter``` to decompose the seperable filter to two, 1D filters.
    -  Implement ```applyImageSepFilter``` function. You can use ```applyImageFilter``` function implemented in Applying Convolutional Filters (Hint: two lines of code)
    -  Performe convolution on ```img``` matrix and measure the time for ```decomposeSeparableFilter``` and ```applyImageSepFilter```.
    -  Repeat the test with several increasing filter sizes.
    -  measure the elapsed time for each filtering. To get the start and end time, use time.time()
    -  Plot the results in a graph (elapsed time) vs (filter size).
    -  What is your conclusion ? Is filtering with separable filters always faster than with non-separable ones?

```python
def decomposeSeparableFilter(F):
    h = [1]
    s = len(F)
    for i in range(1,s):
            h.append(np.sum(F[:,i])/(np.sum(F[:,0])))
    h = np.asmatrix(np.array(h))
    v = np.asmatrix(F[:,0]).transpose()
    return v, h

def applyImageSepFilter(I, F_v, F_h):
    # ******************************************
    # Implement the convolution with the two 1D filters and
    # assign the result to 'output' variable
    # Hint: you only need two lines of code
    # ******************************************
    ### your code here
    #
    # after implementation remove the line below
    raise ValueError('Not implemented yet.')
    # ******************************************

    return output
```

```python
# Sample image
img = cv2.imread('images/res1.png',0)
img = img[150:200,150:200]
# We crop the image, becasue larger images take considerably longer time to
# perform convolution with large filters

# Filter sizes to try out.
# Since we consider filter with odd heigh and width,
# we start from 3 and increment it by 2 till 29.
filter_sizes = range(3,30,2)
```

```python
# ******************************************
# Perform the comparison here
# Hint: Iterate through the filter_sizes and in each iteration perform convolution with the 2D and 1D filters
#         using applyImageFilter and applyImageSepFilter functions respetively. Measure the time for each execution.
#         Store the execution times and plot them at the end
# Note: Performing convolutions with larger filters could take longer time (sometimes more than a minute)
# ******************************************
# your code here
# after implemntation remove the line below
raise ValueError('Not implemented yet.')
# ******************************************
```

```python

```

ExerciseSession2/.ipynb_checkpoints/Convolution, Filtering and Gradients-checkpoint.ipynb


```python
import numpy as np
import scipy as scip
import sympy as sm
import ipywidgets as widgets
from scipy import linalg
from scipy import optimize
from scipy import interpolate
import pandas as pd
import sympy as sym
import matplotlib.pyplot as plt
from matplotlib import cm
%matplotlib inline
from mpl_toolkits.mplot3d import Axes3D
sym.init_printing(use_unicode=True)
from scipy import interpolate
```

# 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).

In order to do this assignment, packages are imported, and to the values we are given, the values of the function is defnied.

```python
#Importing packages
from scipy import interpolate
%matplotlib inline
import matplotlib.pyplot as plt
import ipywidgets as widgets
```

```python
#Defining solutions
def c(h,l,w,b):
    if l == 1:
        return  w*h*l
    elif l == 0:
        return  b

def utility(h,l,rho,gamma,w,b):
    return c(h,l,w,b)**(1-rho)/(1-rho) - gamma*l

def v2(h2,l2,rho,gamma,w,b):
    return utility(h2,l2,rho,gamma,w,b)

def v1(h1,l1,v2_interp,rho,gamma,w,b):

    #This is the result for low-skill
    h2_low = h1 + l1
    v2_low = v2_interp([h2_low])[0]

    #This is the result for high-skill
    h2_high = h1 + l1 + Delta
    v2_high = v2_interp([h2_high])[0]

    #This is the expected value for v2
    v2 = 0.5*v2_low + 0.5*v2_high

    #This is the total value
    return utility(h1,l1,rho,gamma,w,b) + beta*v2
```

There is taken great inspiration from problem set 7 for this section

Now, that the functions have been defined, we move on to defining our solution functions

```python
#Defining solutions
def solve_period_2(h_vec,rho,gamma,w,b):

    #Grids
    v2_vec =  np.empty(100)
    l2_vec = np.empty(100)
    v2_vec_unemp = np.empty(100)

    #Loops
    for i, h2 in enumerate(h_vec):

        if (v2(h2,1,rho,gamma,w,b) > v2(h2,0,rho,gamma,w,b)) == True:
            l2_vec[i] = 1
        else:
            l2_vec[i] = 0

        #List
        v2_vec[i] = v2(h2,l2_vec[i],rho,gamma,w,b)
        v2_vec_unemp[i] = utility(h2,0,rho,gamma,w,b)

    return v2_vec, l2_vec, v2_vec_unemp

#Solve
v2_vec,l2_vec,v2_vec_unemp =  solve_period_2(h_vec,rho,gamma,w,b)
```

Now, we move on to plotting the function to see the utility of the worker, given the

```python
fig = plt.figure()
ax = fig.add_subplot(1,1,1)
ax.plot(h_vec, v2(h_vec,1,rho,gamma,w,b), color = "g")
ax.plot(h_vec, v2_vec_unemp, color = "r")
ax.grid()
ax.set_xlabel('Human capital, $h_2$')
ax.set_ylabel('Value of living, $v_2$')
ax.set_title('Value of living given human capital in period 2')
plt.legend(('Working, $l_2=1$','Not working, $l_2=0$'),loc='lower center')
plt.show()
index = np.where(l2_vec == 1)[0][0];
print('The graphs intersect at h ≥ ' + str(round(h_vec[index],3)))
```

As we see from the graph, the values the worker is affiliated for utility, is negative whether she is working or not. She does though receive less negative utility, to a higher skill level than the intersect point, 0.567.

Now, a different graph will be made, which is the labour supply in regard to the human capital.

```python
# Plotting LS as a function of HC, period 2
fig = plt.figure()
ax = fig.add_subplot(1,1,1)
ax.plot(h_vec, l2_vec,rho,gamma,w,b,color = "r")
ax.grid()
ax.set_xlabel('HC, $h_2$')
ax.set_ylabel('LS, $l_2$')
ax.set_title('Labour based on skill, period 2')
plt.show()
```

Here, the graph tells us, that if the worker is in posession of HC which is 0.55 or above, the worker will be motivated to work in the second period, which will provide the worker with a higher amount of utility. However, if the worker is not in enough posession of HC, she will not choose to work, and therefore will live on the unemployment benefits, since the disutility outweighs the utility of consumption.

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

As done in the first assignment, the solution functions will now be defined.

```python
#Interpolator
v2_interp = interpolate.RegularGridInterpolator([h_vec],v2_vec,bounds_error=False,fill_value=None)

def solve_period_1(h_vec, v2_interp,rho,gamma,w,b):

    #Grids
    v1_vec =np.empty(100)
    l1_vec = np.empty(100)
    v1_vec_unemployed = np.empty(100)

    #Loop
    for i,h1 in enumerate(h_vec):

        #Utility
        if (v1(h1,1,v2_interp,rho,gamma,w,b) > v1(h1,0,v2_interp,rho,gamma,w,b)) == True:
            l1_vec[i] = 1
        else:
            l1_vec[i] = 0

        #List
        v1_vec[i] = v1(h1,l1_vec[i],v2_interp,rho,gamma,w,b)
        v1_vec_unemployed[i] = utility(h1,0,rho,gamma,w,b)

    return v1_vec, l1_vec, v1_vec_unemployed

#Solve
v1_vec, l1_vec, v1_vec_unemployed =  solve_period_1(h_vec, v2_interp,rho,gamma,w,b)
```

Now, the labour supply function on HC, will be illustrated with a plot

```python
#LS as func of HC, periode 1
fig = plt.figure()
ax = fig.add_subplot(1,1,1)
ax.plot(h_vec, l1_vec,rho,gamma,w,b,color = "r")
ax.grid()
ax.set_xlabel('HC, $h_1$')
ax.set_ylabel('LS, $l_1$')
ax.set_title('Labour based on skill, period 1')
plt.show()
```

Here, the graph tells us, that the skill level required to motivate the worker enough to want to work, is around 0.35. In addition, since the worker has a higher chance og evolving through working in the first period, it makes sense that a higher requirement is present in the second period, which was seen on the previous graphs.

Next up is illustrating the function, based on HC.

```python
#Value based on HC
fig = plt.figure()
ax = fig.add_subplot(1,1,1)
ax.plot(h_vec, v1_vec, color = "green")
ax.plot(h_vec,v1_vec_unemployed, color = 'red')
ax.grid()
ax.set_xlabel('HC, $h_1$')
ax.set_ylabel('Value, $v_1$')
ax.set_title('Value based on HC, period 1')
plt.legend(('Working, $l_1=1$','Not working, $l_1=0$'),loc='LC')
plt.show()
index = np.where(l1_vec == 1)[0][0];
print('The graphs intersect at h ≥ ' + str(round(h_vec[index],3)))
```

Here the graph tells us, that in order to motivate the worker to work, a level of human capital is required to be 0.355 or higher.

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

In order to answer this question, the effects of changing wage, chance of avoiding work, and the benefits available if choosing not to work for both periods will be investigated.

```python
#Defining the function
def int_fig(w,b,gamma):

    #Implementing previous solutions
    v1_vec, l1_vec, v1_vec_unemployed =  solve_period_1(h_vec, v2_interp,rho,gamma,w,b)
    v2_vec,l2_vec, v2_vec_unemployed =  solve_period_2(h_vec,rho,gamma,w,b)

    #Setting baseline for the illustration
    fig, ((ax1, ax3), (ax2, ax4)) = plt.subplots(nrows=2,  ncols=2, sharex=True, figsize=(16,12))

    #Period 1
    ax1.plot(h_vec,v1_vec_unemployed, label='Utility if $l_1=0$', color = 'green')
    ax1.plot(h_vec,v1_vec, label='Utility if $l_1=1$', color = 'red')
    ax1.grid()
    ax1.legend(loc='lower right')
    ax1.set_xlabel('HC, $h_1$')
    ax1.set_ylabel('Value, $v_1$')
    ax1.set_title('Value, period 1');

    #HC/LS, period 1
    ax2.plot(h_vec,l1_vec, label='LS',color = "r")
    ax2.grid()
    ax2.set_xlabel('HC, $h_1$')
    ax2.set_ylabel('LS, $l_1$')
    ax2.set_title('LS based on HC, period 1');

    #Value, period 2
    ax1.plot(h_vec,v2_vec_unemp, label='Utility if $l_2=0$', color = 'green')
    ax3.plot(h_vec,v2_vec, label='Utility if $l_2=1$', color = 'red')
    ax3.grid()
    ax3.legend(loc='lower right')
    ax3.set_xlabel('HC, $h_2$')
    ax3.set_ylabel('Value, $v_2$')
    ax3.set_title('Value, period 2');

    #HC/LS, period 2
    ax4.plot(h_vec,l2_vec, label='Labor supply',color = "r")
    ax4.grid()
    ax4.legend(loc='lower right')
    ax4.set_xlabel('HC, $h_2$')
    ax4.set_ylabel('LS, $l_2$')
    ax4.set_title('LS based on HC, period 2');

# Widgets for the chosen parameters
widgets.interact(int_fig,
    b = widgets.FloatSlider(value=1,description='Unemployment benefits', min=0.1,max=5, step=0.1, ),
    w = widgets.FloatSlider(value=2,description='Wage rate',min=0.1,max=5, step=0.1,),
    gamma = widgets.FloatSlider(value=0.1,description='Aversion to work', min=0,max=5, step=0.1,));
```

    interactive(children=(FloatSlider(value=2.0, description='Wage rate', max=5.0, min=0.1), FloatSlider(value=1.0…

Its not impossible, that the worker chooses not to work in both of the periods, since the difference between the benefits gained from not working, and working not differ enough. This is also quite intuitive. However, a situation where the benefits grant a higher wage than gained from working, and the worker till chooses to work, would be possible if to achieve a high level of HC.
Therefore, in conclusion, there is no concrete result. If the worker chooses to work or not, relies on the levels of wages, the benefits, and the disutility/utility.

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

In order to solve the equilibrium values, the symbols are defined.

```python
#Definig symbols
alpha = sm.symbols('alpha')
h = sm.symbols('h')
b = sm.symbols('b')
phi = sm.symbols('phi')
gamma = sm.symbols('gamma')
r= sm.symbols('r')
vt= sm.symbols('v_t')
yt= sm.symbols('y_t')
pi= sm.symbols('pi')
phi= sm.symbols('phi')
st= sm.symbols('s_t')
pit_1= sm.symbols('pi_t-1')
yt_1=sm.symbols('y_t-1')
st_1=sm.symbols('s_t-1')
```

Next, is the definition of AD and SRAS

```python
#Defining
AD= (1/(h*alpha))*(vt-(yt*(1+b*alpha)))
SRAS=pit_1+(gamma*yt)-(phi*gamma*yt_1)+st+(phi*st_1)
```

Now, we implement the possibility of printing results out in the document

```python
#Printing
sm.init_printing(use_unicode=True)
```

```python
AD1=(1/(par['h']*par['alpha']))*(vt-(yt*(1+par['b']*par['alpha'])))
AD1
```

```python
SRAS1=pit_1+(par['gamma']*yt)-(par['phi']*par['gamma']*yt_1)+st+(par['phi']*st_1)
SRAS1
```

The short term equilibrium is dependant on the aggregated supply, which in this case is equal to the aggregated demand. This means, that when we set those two factors equal to one another, and solve for y, we will find the output of the equilibrium.

```python
#Output
AD_SRAS=sm.Eq(AD,SRAS)
ytstar=sm.solve(AD_SRAS, yt)
ytstar
```

To solve the inflation, the recently acquired AD y* is inserted

```python
#Inflation
pitstar = 1/(h*alpha)*(vt-(1+b*alpha)*ytstar[0])
pitstar
```

```python
#Output
AD1_SRAS1=sm.Eq(AD1,SRAS1)
ytstar1=sm.solve(AD1_SRAS1, yt)
ytstar1
```

```python
#Inflation
pitstar1 = 1/(par['h']*par['alpha'])*(vt-(1+par['b']*par['alpha'])*ytstar1[0])
pitstar1
```

Now, we can conclude that the equilibrium output and the inflation hereof is depoendant on the inflation in the previous periods, and on whether the demand or supply effects have caused any disturbance.

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

To do this, $y_t$,  and $\pi_t$ is solved for numerically.

```python
#Defining the function
yt_eq = sm.lambdify((alpha,h,b,phi,gamma,vt,st,pit_1,yt_1,st_1), ytstar[0])
```

```python
#Defining the function
pit_eq = sm.lambdify((alpha,h,b,phi,gamma,vt,st,pit_1,yt_1,st_1), pitstar)
```

Now, as we know gamma is equal to 0.075, we can now

```python
#Output
yt_eq(par['alpha'], par['h'], par['b'], par['phi'], par['gamma'], 0, 0, 0, 0, 0)
```

```python
#Inflation
pit_eq(par['alpha'], par['h'], par['b'], par['phi'], par['gamma'], 0, 0, 0, 0, 0)
```

As we want to see this illustrated, the AD an SRAS are defined as functions.

```python
def ADcurve(h, alpha, yt, vt, b):
    return (1/(h*alpha))*(vt-(y*(1+b*alpha)))

def SRAScurve(pit_1, gamma, yt, yt_1, st, phi, st_1):
    return pit_1+(gamma*yt)-(phi*gamma*yt_1)+st+(phi*st_1)
```

```python
#Inserting values
y=np.linspace(-0.1,0.1,100)
AD_plot=ADcurve(0.5, 5.76, y, 0, 0.5)
SRAS_plot=SRAScurve(0, 0.075, y, 0, 0, 0, 0)

#Plotting
plt.plot(y,AD_plot,label='AD')
plt.plot(y,SRAS_plot,label='SRAS')
plt.grid(True)

plt.title('AD and SRAS')
plt.xlabel('$y_t$')
plt.ylabel('$\pi_t$')
```

The equilibrium for short term is given where the two graphs intersect. This is given at (0,0), which does correspond to the numerical solution found earlier.

The previous solution was for $v_t$ = 0.0.
Now, we find the solution for $v_t$ = 0.1.

First numerically

```python
#Output
yt_eq(par['alpha'], par['h'], par['b'], par['phi'], par['gamma'], 0.1, 0, 0, 0, 0)
```

```python
#Inflation
pit_eq(par['alpha'], par['h'], par['b'], par['phi'], par['gamma'], 0.1, 0, 0, 0, 0)
```

And now illustrative

```python
#Creating a shift in the AD curve
AD_plot1=ADcurve(0.5, 5.76, y, 0.1, 0.5)

y = np.linspace(-0.1,0.1,100)
plt.plot(y,AD_plot1,label='AD1')
plt.plot(y,AD_plot,label='AD0')
plt.grid(True)

plt.title('Positive shift in the AD-curve')
plt.xlabel('$y_t$')
plt.ylabel('$\pi_t$')
```

It is noticed, that for any given value of $y_t$,  $\pi_t$ is now at a higher value, than prior to the shift in the AD curve. Therefore, we can conlude, that in the new equilibrium, both $y_t$ and  $\pi_t$ will be at higher values.

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

In order to see how the economy will evolve, the processes(AR) are first defined.

```python
#Defining processes
def v_func(vt_1, x):
    return par['delta']*vt_1+x

def s_func(st_1,c):
    return par['omega']*st_1+c
```

```python
y_lambdified = sm.lambdify((pit_1, st, vt, phi, st_1, yt_1), ytstar1)
pi_lambdified = sm.lambdify((pit_1, st, yt, phi, st_1, yt_1), pitstar1)
```

Now, a simulation with 100 periods will be run, where vectors without substance will be created in order to compensate, which will be filled through loops.

```python
T=100

#Simulating allocated data
y = [0]
pi = [0]
v = [0]
s = [0]
c = np.zeros(T)
x = np.zeros(T)
x[1] = 0.1

for t in range(1,T):
    v.append(v_func(v[t-1], x[t]))
    s.append(s_func(s[t-1], c[t]))
    y.append(yt_eq(par['alpha'], par['h'],par['b'],par['phi'],par['gamma'],v[t],s[t],pi[t-1],y[t-1],s[t-1]))
    pi.append(pit_eq(par['alpha'], par['h'],par['b'],par['phi'],par['gamma'],v[t],s[t],pi[t-1],y[t-1],s[t-1]))
```

Now, the output gap over 100 periods will be illustrated.

```python
plot = plt.figure(figsize=(15,10))
ax = plot.add_subplot(311)
plt.grid(True)

ax.plot(y,label='$\y$',color = "r")
ax.set_title('Output gap development')
ax.set_xlabel('$time$')
ax.set_ylabel('$y$')
```

We see a spike in the gap, which occurs at (0.1,0.024.). This is in correspondence with a shock in the demand, which results in a increase in the output. However, we see the gap stabilizing over time, which can be corresponded back to that we have $x_t$ = 0.

Next up is the illustration for the inflation gap, also for 100 periods.

```python
plot = plt.figure(figsize=(15,10))
ax = plot.add_subplot(311)
plt.grid(True)

ax.plot(pi,label='$\pi$',color = "r")
ax.set_title('Inflation gap development')
ax.set_xlabel('$time$')
ax.set_ylabel('$\pi$')
```

We see a smiliar response, with a spike followed by a stabilizing curve.
What both grahps share, is that they both converge towards 0, and both spikes are caused by shocks in supply or demand.

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

First things first, we create a simulation of the AS-AD model, for 1000 periods.

In order to do this, we do first need to define the functions.

```python
#Setting periods
np.random.seed(12345)
T=1000

#Creating shocks
x1 = np.random.normal(loc=0,scale=par['sigma_x'],size=T)
c1 = np.random.normal(loc=0,scale=par['sigma_c'],size=T)

#Allocating data
y1 = [0]
pi1 = [0]
v1 = [0]
s1 = [0]
x[1] = 0.1

for t in range(1,T):
    v1.append(v_func(v1[t-1], x1[t]))
    s1.append(s_func(s1[t-1], c1[t]))
    y1.append(yt_eq(par['alpha'], par['h'],par['b'],par['phi'],par['gamma'],v1[t],s1[t],pi1[t-1],y1[t-1],s1[t-1]))
    pi1.append(pit_eq(par['alpha'], par['h'],par['b'],par['phi'],par['gamma'],v1[t],s1[t],pi1[t-1],y1[t-1],s1[t-1]))
```

Now, we can illustrate the model.

```python
plot = plt.figure(figsize=(15,15))
ax = plot.add_subplot(311)

ax.plot(y1,label='$output$')
ax.plot(pi1,label='$inflation$')

ax.set_title('Output gap and inflation development')
ax.set_xlabel('$time$')
ax.set_ylabel('$\pi/y$')
ax.legend(loc='upper right');
```

Now we move on to calcuating the statistics.

1. Variance of $y_t$, $var(y_t)$

```python
y_var=np.var(y1)
y_var
```

```python
print(f'Variance of y is, {y_var:.2}')
```

    Variance of y is, 1.9

2. Variance of $\pi_t$, $var(\pi_t)$

```python
pi_var=np.var(pi1)
pi_var
```

```python
print(f'Variance of pi is, {pi_var:.2}')
```

    Variance of pi is, 0.86

3. Correlation between $y_t$ and $\pi_t$, $corr(y_t,\pi_t)$

```python
np.corrcoef(y1,pi1)
```

    array([[ 1.        , -0.17556771],
           [-0.17556771,  1.        ]])

```python
print(f'Correlation between y and pi is, {np.corrcoef(y1,pi1)[0,1]:.2}')
```

    Correlation between y and pi is, -0.18

4. Auto-correlation between $y_t$ and $y_{t-1}$, $corr(y_t,y_{t-1})$

In the previous 3 cases, it was just plug and play. However, we need to take the auto-correlation into account here, which means we have to defining a function for is.

```python
#Defining function
def autocorr(x, t=1):
    return np.corrcoef(np.array([x[:-t], x[t:]]))

#Fining the autocorrelation
autocorr(y1)
```

    array([[1.        , 0.78020678],
           [0.78020678, 1.        ]])

```python
print(f'Autocorrelation in y is, {autocorr(pi1)[0,1]:.2}')
```

    Autocorrelation in y is, 0.97

5. Auto-correlation between $\pi_t$ and $\pi_{t-1}$, $corr(\pi_t,\pi_{t-1})$

Since we defined the function prior to this question, we can just plug it in.

```python
autocorr(pi1)
```

    array([[1.       , 0.9720786],
           [0.9720786, 1.       ]])

```python
print(f'Autocorrelation in pi is, {autocorr(y1)[0,1]:.2}')
```

    Autocorrelation in pi is, 0.78

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

To answer this question, a simulation which will run different values for the output gap, and different values for the inflation gap will be implemented.

```python
#Seed
T=1000
x_3_vec = np.random.normal(scale=par['sigma_x'], loc=0, size=T)
c_3_vec = np.random.normal(scale=par['sigma_c'], loc=0, size=T)
```

As the seed has been set, we now move on to defining the simulation.

```python
#Defining simulation
def simulate_phi(phi):
    #Vectors
    v_3_vec = [0]
    s_3_vec = [0]
    y_3_vec = [0]
    pi_3_vec = [0]

    #Range
    for t in range(1,T):
        v_3_vec.append(v(v_3_vec[t-1], x_3_vec[t]))
        s_3_vec.append(s(s_3_vec[t-1], c_3_vec[t]))
        y_3_vec.append(y_star1(par['alpha'], par['b'],par['gamma'],par['h'],phi,y_3_vec[t-1],s_3_vec[t-1],pi_3_vec[t-1],s_3_vec[t],v_3_vec[t]))
        pi_3_vec.append(pi_star1(par['alpha'], par['b'],par['gamma'],par['h'],phi,y_3_vec[t-1],s_3_vec[t-1],pi_3_vec[t-1],s_3_vec[t],v_3_vec[t]))
    corr_2_y_pi = np.corrcoef(y_3_vec,pi_3_vec)[0,1]
    return corr_2_y_pi
```

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

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

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

In order to answer this question, 3 plots will be illustrated

```python
#Plotting histograms
fig = plt.figure(figsize=(15,5))
ax = fig.add_subplot(1,3,1)
ax.hist(betas[:,0],bins=200,histtype='bar',color = "r")
ax.set_xlabel('$Beta_1$')
ax.set_ylabel('Consumption')
ax.set_title('Good Across Agent 1')
ax.set_ylim([0, 1250])

ax = fig.add_subplot(1,3,2)
ax.hist(betas[:,1],bins=200,histtype='bar',color = "r")
ax.set_xlabel('$Beta_2$')
ax.set_title('Good Across Agent 2')
ax.set_ylim([0, 1250])

ax = fig.add_subplot(1,3,3)
ax.hist(betas[:,2],bins=200,histtype='bar',color = "r")
ax.set_xlabel('$Beta_3$')
ax.set_title('Good Across Agent 3')
ax.set_ylim([0, 1250]);
```

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

In order to do this question, the functions will first be defined, and then plotted.

```python
#Defining demand functions, p3=1
def demand_good1(p1,p2,e1,e2,e3,betas):
    I = e1*p1+e2*p2+e3
    return betas[:,0]*I/p1

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

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

#Defining excess demand functions
def excess1(p1,p2,e1,e2,e3,betas):
    demand = np.sum(demand_good1(p1,p2,e1,e2,e3,betas))
    supply = np.sum(e1)
    excess1 = demand - supply
    return excess1

def excess2(p1,p2,e1,e2,e3,betas):
    demand = np.sum(demand_good2(p1,p2,e1,e2,e3,betas))
    supply = np.sum(e2)
    excess2 = demand - supply
    return excess2

#Defining array of prices
p1_s = np.linspace(0.1,10,100)
p2_s = np.linspace(0.1,10,100)

#Grids for excess demand
excess_grid1 = np.empty((100,100))
excess_grid2 = np.empty((100,100))

#Transforming price-vectors
p1_grid, p2_grid = np.meshgrid(p1_s,p2_s,indexing='ij')

#Calculations
for i,p1 in enumerate(p1_s):
    for j,p2 in enumerate(p2_s):
        excess_grid1[i,j] = excess1(p1,p2,e1,e2,e3,betas)
        excess_grid2[i,j] = excess2(p1,p2,e1,e2,e3,betas)
```

Now, the plots will be illustrated

```python
# We can now plot our results
fig = plt.figure(figsize=(15,5))
ax = fig.add_subplot(1,2,1, projection='3d')
ax.plot_surface(p1_grid, p2_grid, excess_grid1,color = "r")
ax.invert_xaxis()
ax.set_title('Excess demand, Good Across Agent 1')
ax.set_xlabel('$p_1$')
ax.set_ylabel('$p_2$')

ax = fig.add_subplot(1,2,2, projection='3d')
ax.plot_surface(p1_grid, p2_grid, excess_grid2,color = "r")
ax.invert_xaxis()
ax.set_title('Excess demand, Good Across Agent 2')
ax.set_xlabel('$p_1$')
ax.set_ylabel('$p_2$');
```

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

In order to answer this question, the tatonnement will first be defined, thereafter the demands will be calculated.

```python
def tatonnement(p1,p2,e1,e2,e3,betas,tol=1e-7,kappa=0.2,prints=True):
  #Defining initial excess demands
    z_1 = excess1(p1,p2,e1,e2,e3,betas)
    z_2 = excess2(p1,p2,e1,e2,e3,betas)
    t = 1 #Iteration counter

    #The loop
    while t < 12500:

        #Calculating initial excess demands
        z_1 = excess1(p1,p2,e1,e2,e3,betas)
        z_2 = excess2(p1,p2,e1,e2,e3,betas)

        #Tolerance level
        if abs(z_1)<tol and abs(z_2)<tol:
            if prints:
                print(f'\nIn the Walras equilibrium we have: p1 = {p1:.2f} and p2 = {p2:.2f}')
            p1_eq = p1
            p2_eq = p2
            return p1_eq, p2_eq

        #No convergence
        else:
            p1 += kappa*z_1/N
            p2 += kappa*z_2/N

        #Iteration
        if prints:
            if t <= 10 or t%1000==0:
                print(f'Iter {t:6.0f}: Excess good 1 = {z_1:10.2f}, Excess good 2 = {z_2:10.2f} => p1 = {p1:10.2f}, p2 = {p2:10.2f}')

        t += 1

    #Printing based on outcome
    if t == 12500:
        text = 'No convergence \n'
        print(text)
        return None, None
```

In order to actually find the walras-equilibrium prices, an estimated guess will be taken, and then the function will be called to actually find a result.

```python
#Guess
p1 = 5
p2 = 1

#Calling function
p1_eq, p2_eq = tatonnement(p1,p2,e1,e2,e3,betas)
```

    Iter      1: Excess good 1 =   -5009.04, Excess good 2 =   40450.64 => p1 =       4.98, p2 =       1.16
    Iter      2: Excess good 1 =   -3919.95, Excess good 2 =   29459.03 => p1 =       4.96, p2 =       1.28
    Iter      3: Excess good 1 =   -3118.04, Excess good 2 =   23192.52 => p1 =       4.95, p2 =       1.37
    Iter      4: Excess good 1 =   -2482.75, Excess good 2 =   19014.91 => p1 =       4.94, p2 =       1.45
    Iter      5: Excess good 1 =   -1960.44, Excess good 2 =   15991.63 => p1 =       4.93, p2 =       1.51
    Iter      6: Excess good 1 =   -1521.24, Excess good 2 =   13688.79 => p1 =       4.93, p2 =       1.57
    Iter      7: Excess good 1 =   -1146.30, Excess good 2 =   11872.04 => p1 =       4.92, p2 =       1.61
    Iter      8: Excess good 1 =    -822.76, Excess good 2 =   10401.69 => p1 =       4.92, p2 =       1.66
    Iter      9: Excess good 1 =    -541.35, Excess good 2 =    9188.39 => p1 =       4.92, p2 =       1.69
    Iter     10: Excess good 1 =    -295.10, Excess good 2 =    8171.85 => p1 =       4.92, p2 =       1.73
    Iter   1000: Excess good 1 =      31.98, Excess good 2 =      11.93 => p1 =       6.46, p2 =       2.60
    Iter   2000: Excess good 1 =       0.82, Excess good 2 =       0.30 => p1 =       6.49, p2 =       2.62
    Iter   3000: Excess good 1 =       0.02, Excess good 2 =       0.01 => p1 =       6.49, p2 =       2.62
    Iter   4000: Excess good 1 =       0.00, Excess good 2 =       0.00 => p1 =       6.49, p2 =       2.62
    Iter   5000: Excess good 1 =       0.00, Excess good 2 =       0.00 => p1 =       6.49, p2 =       2.62
    Iter   6000: Excess good 1 =       0.00, Excess good 2 =       0.00 => p1 =       6.49, p2 =       2.62

    In the Walras equilibrium we have: p1 = 6.49 and p2 = 2.62

Here we see that, the Walras equilibrium in this case is 6.49 for p1, and 2.62 for p2.

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

In order to plot the distribution of utility within the walras euquilibrium, we again first define the functions, and then calculate its variance, whereafter it is illustrated.

```python
def u(p1, p2, e1, e2, e3, betas, gamma):

    #Definig income
    I = p1*e1+p2*e2+e3

    #Defining demand
    demand1 = betas[:,0]*(I/p1)
    demand2 = betas[:,1]*(I/p2)
    demand3 = betas[:,2]*I

    #Calculating utility
    u = (demand1**betas[:,0]+demand2**betas[:,1]+demand3**betas[:,2])**gamma
    return u
```

Now, a function which creates a given vector of our given utilites is constructed.

```python
#Defining the function
u_vec = u(p1_eq, p2_eq, e1, e2, e3, betas, gamma)

#Mean and Variance
mean = np.mean(u_vec)
var = np.var(u_vec)
print(f'Mean: {mean:.2f}')
print(f'Variance: {var:.2f}')
```

    Mean: 2.38
    Variance: 0.21

And finally, we can now plot the distribution fo the utilies.

```python
#Plot
plt.hist(u_vec, bins=500,color = "r")
plt.title('Utility distribution')
plt.xlabel('Utility')
plt.ylabel('Consumers')
```

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

Here we will call to the same tatonnement in question 3.

```python
e1_new = np.ones(N)*np.mean(e1)
e2_new = np.ones(N)*np.mean(e2)
e3_new = np.ones(N)*np.mean(e3)
```

```python
p1 = 5
p2 = 1

p1_new, p2_new = tatonnement(p1,p2,e1_new,e2_new,e3_new,betas)
```

    Iter      1: Excess good 1 =   -5020.74, Excess good 2 =   40503.83 => p1 =       4.98, p2 =       1.16
    Iter      2: Excess good 1 =   -3930.11, Excess good 2 =   29491.20 => p1 =       4.96, p2 =       1.28
    Iter      3: Excess good 1 =   -3127.21, Excess good 2 =   23215.68 => p1 =       4.95, p2 =       1.37
    Iter      4: Excess good 1 =   -2491.16, Excess good 2 =   19032.95 => p1 =       4.94, p2 =       1.45
    Iter      5: Excess good 1 =   -1968.23, Excess good 2 =   16006.34 => p1 =       4.93, p2 =       1.51
    Iter      6: Excess good 1 =   -1528.51, Excess good 2 =   13701.13 => p1 =       4.93, p2 =       1.57
    Iter      7: Excess good 1 =   -1153.11, Excess good 2 =   11882.60 => p1 =       4.92, p2 =       1.62
    Iter      8: Excess good 1 =    -829.18, Excess good 2 =   10410.87 => p1 =       4.92, p2 =       1.66
    Iter      9: Excess good 1 =    -547.41, Excess good 2 =    9196.44 => p1 =       4.92, p2 =       1.69
    Iter     10: Excess good 1 =    -300.84, Excess good 2 =    8178.99 => p1 =       4.92, p2 =       1.73
    Iter   1000: Excess good 1 =      31.80, Excess good 2 =      11.88 => p1 =       6.45, p2 =       2.60
    Iter   2000: Excess good 1 =       0.81, Excess good 2 =       0.30 => p1 =       6.49, p2 =       2.62
    Iter   3000: Excess good 1 =       0.02, Excess good 2 =       0.01 => p1 =       6.49, p2 =       2.62
    Iter   4000: Excess good 1 =       0.00, Excess good 2 =       0.00 => p1 =       6.49, p2 =       2.62
    Iter   5000: Excess good 1 =       0.00, Excess good 2 =       0.00 => p1 =       6.49, p2 =       2.62
    Iter   6000: Excess good 1 =       0.00, Excess good 2 =       0.00 => p1 =       6.49, p2 =       2.62

    In the Walras equilibrium we have: p1 = 6.49 and p2 = 2.62

It is noticed, that the prices in this case is the excat same as in q3.

Now, a new funtion is defined, to calculate the new means and variances, which is required in order to plot the difference in gamma.

```python
def u_new(p1, p2, e1, e2, e3, betas, gamma):

    #Definig income
    I = p1*e1+p2*e2+e3

    #Defining demand
    demand1_new = betas[:,0]*(I/p1)
    demand2_new = betas[:,1]*(I/p2)
    demand3_new = betas[:,2]*I

    #Calculating utility
    u = (demand1_new**betas[:,0]+demand2_new**betas[:,1]+demand3_new**betas[:,2])**gamma
    return u_new
```

The function in order to create the vector of utilites is now defined, along with finding the new mean and variance.

```python
#Defining funtion
u_vec_new = u(p1_eq, p2_eq, e1, e2, e3, betas, 1.5)

#New mean and variance
mean = np.mean(u_vec_new)
var = np.var(u_vec_new)
print(f'Mean: {mean:.2f}')
print(f'Variance: {var:.2f}')
```

    Mean: 5.22
    Variance: 3.71

This tells us, that even to unchanged prices, gamma will influence the mean and variance values. We see an 0.7 increase in gamma, which results in a 2.84 incerase in mean, and a flat 3.5 increase in variance.

Now, the distribution of utility is illustrated.

```python
fig = plt.figure(figsize=(15,5))
plt.hist(u_vec, bins=200, label='$\gamma$ = 0.8')
plt.hist(u_vec_new, bins=200, color='red', label='$\gamma$ = 1.5')
plt.legend()
plt.title('Distribution of utility')
plt.xlabel('Utility')
plt.ylabel('Consumption');
```

This makes it unquestionable, that gamma indeed does play a role.


# HW 6

## Imports

```python
import nbtools
nbtools.setup_nb()
```

```python
import itertools
from astropy import units, constants
import sympy
import pandas
from scipy import integrate
import numpy
import plotly.express as px
from plotly import graph_objects as go

from sympy.diffgeom import Manifold, Patch
from pystein import coords, metric, curvature, geodesic
from pystein.utilities import tensor_pow as tpow, full_simplify, boundary_filter
```

## Utilities

```python
import plotly.colors

def get_continuous_color(colorscale, intermed):
    """
    Plotly continuous colorscales assign colors to the range [0, 1]. This function computes the intermediate
    color for any value in that range.

    Plotly doesn't make the colorscales directly accessible in a common format.
    Some are ready to use:

        colorscale = plotly.colors.PLOTLY_SCALES["Greens"]

    Others are just swatches that need to be constructed into a colorscale:

        viridis_colors, scale = plotly.colors.convert_colors_to_same_type(plotly.colors.sequential.Viridis)
        colorscale = plotly.colors.make_colorscale(viridis_colors, scale=scale)

    :param colorscale: A plotly continuous colorscale defined with RGB string colors.
    :param intermed: value in the range [0, 1]
    :return: color in rgb string format
    :rtype: str
    """
    if len(colorscale) < 1:
        raise ValueError("colorscale must have at least one color")

    if intermed <= 0 or len(colorscale) == 1:
        return colorscale[0][1]
    if intermed >= 1:
        return colorscale[-1][1]

    for cutoff, color in colorscale:
        if intermed > cutoff:
            low_cutoff, low_color = cutoff, color
        else:
            high_cutoff, high_color = cutoff, color
            break

    # noinspection PyUnboundLocalVariable
    return plotly.colors.find_intermediate_color(
        lowcolor=low_color, highcolor=high_color,
        intermed=((intermed - low_cutoff) / (high_cutoff - low_cutoff)),
        colortype="rgb")
```

## Theory

### A2

```python
M = Manifold('M', dim=4)
P = Patch('origin', M)

t, r, theta, phi, k = sympy.symbols('t r theta phi k')
cs = coords.CoordSystem('spherical', P, [t, r, theta, phi])
dt, dr, dtheta, dphi = cs.base_oneforms()
a = sympy.Function('a')(t)

ds2 = - tpow(dt, 2) + a ** 2 * ((1 / (1 - k * r ** 2)) * tpow(dr, 2) +
                                r**2 * tpow(dtheta, 2) +
                                r ** 2 * sympy.sin(theta) ** 2 * tpow(dphi, 2))
g = metric.Metric(twoform=ds2)
g
```

```python
G_rr = curvature.einstein_tensor_component(1, 1, g)
```

```python
sympy.simplify(G_rr.doit())
```

```python

```

## Exercises B

### B3

```python
M = Manifold('M', dim=4)
P = Patch('origin', M)

t, r, theta, phi, k = sympy.symbols('t r theta phi k')
cs = coords.CoordSystem('spherical', P, [t, r, theta, phi])
dt, dr, dtheta, dphi = cs.base_oneforms()
a = sympy.Function('a')(t)

ds2 = - tpow(dt, 2) + a ** 2 * ((1 / (1 - k * r ** 2)) * tpow(dr, 2) +
                                r**2 * tpow(dtheta, 2) +
                                r ** 2 * sympy.sin(theta) ** 2 * tpow(dphi, 2))
g = metric.Metric(twoform=ds2)
g
```

```python
christoffels = []
for m, n, l in itertools.product(range(4), range(4), range(4)):
#     print(m, n, l)
    gamma = sympy.simplify(curvature.christoffel_symbol_component(l, m, n, metric=g).doit())
    christoffels.append(((m, n, l), gamma))
christoffels = [x for x in christoffels if x[1]]
```

```python
_ch = [((_mu, _nu, _rho), expr) for (_mu, _nu, _rho), expr in christoffels if _rho == 0]
```

```python
curvature.display_components(_ch)
```

```python
_exs = [expr for (_mu, _nu, _rho), expr in _ch]
_g_ii = [g.inverse.matrix[i, i] for i in range(1, 4)]

_x = sum([_gii * _e for _gii, _e in zip(_g_ii, _exs)])
```

```python
sympy.simplify(_x)
```

```python
sympy.simplify(_x)
```

```python

```

```python
def covariant_deriv(mu, nu, vector, metric):
    symbols = metric.coord_system.base_symbols()

    x_mu = symbols[mu]
    v_nu = vector[nu]

    res = sympy.Derivative(v_nu, x_mu)

    for alpha in range(4):
        v_alpha = vector[alpha]
        gamma_mu_alpha_nu = sympy.simplify(curvature.christoffel_symbol_component(nu, mu, alpha, metric=g).doit())
        res += gamma_mu_alpha_nu * v_alpha

    res = sympy.simplify(res.doit())

    return res
```

```python
eta_vector = [
    0,
    sympy.sqrt(1 - k * r**2) * sympy.cos(theta),
    - sympy.sqrt(1 - k * r**2) * sympy.sin(theta) / r,
    0,
]
```

```python
def cov_deriv_matrix(vector, g):
    components = [4 * [None] for i in range(4)]
    for mu in range(4):
        for nu in range(4):
            del_mu_v_nu = covariant_deriv(mu, nu, vector, g)
            components[mu][nu] = del_mu_v_nu
    return sympy.Matrix(components)
```

```python
cmat = cov_deriv_matrix(eta_vector, g)
```

```python
subs = [
    (sympy.Derivative(a, t), sympy.symbols('\dot{a}')),
    (a, sympy.symbols('a')),
]
```

```python
print((cmat * g.matrix).subs(subs)._repr_latex_())
```

```python
G_tt = curvature.einstein_tensor_component(0, 0, g).doit()
```

```python
full_simplify(G_tt)
```

```python
R = curvature.ricci_scalar(g).doit()
```

```python
full_simplify(R)
```

### B4

```python
ts = numpy.arange(0, 10, 0.1)
consts = numpy.arange(1, 5 + 1, .1)
c_norm = lambda c: (c - 1) / 4.0
```

```python
def psi(t, a_o):
    return (2 / a_o) * numpy.sqrt(t)
```

```python
viridis_colors, _ = plotly.colors.convert_colors_to_same_type(plotly.colors.sequential.Plasma)
colorscale = plotly.colors.make_colorscale(viridis_colors)
```

```python
fig = go.Figure()
for c in consts:
    color = get_continuous_color(colorscale, intermed=c_norm(c))
    fig.add_trace(go.Scatter(x=psi(ts, c), y=ts, line=dict(color=color, dash='solid')))
    fig.add_trace(go.Scatter(x=-psi(ts, c), y=ts, line=dict(color=color, dash='dot')))

fig.update_layout(
#     yaxis_range=[0,10],
#                   xaxis_range=[0,10],
                  width=700,
                  height=700,
                  showlegend=False,
                  title_text=r'$\text{Null Radial Geodesics in FLRW}(t, \psi)$',
                  title_x=0.5,
                  xaxis_title=r'$\psi$',
                  yaxis_title=r'$t/t_o$')
fig.show()
```

```python
# fig.write_image('/Users/jim/repos/tex/homework/courses/PHYS510 - GR I/hw6/figures/fig-b4.pdf')
```

### B6

```python
ts = numpy.arange(0.001, 2.001, 0.001)
a_s = numpy.sqrt(ts)
H_s = 1 / (2 * ts)
R_s =  - 1 / (4 * ts ** 2)
rho_s = 3 / (8 * numpy.pi * ts ** 2)
```

```python
fig = go.Figure()

fig.add_trace(go.Scatter(x=ts, y=a_s, name=r'$a(t)$', line=dict(color='green', dash='solid')))

fig.add_trace(go.Scatter(x=ts, y=H_s, name=r'$H(t)$', line=dict(color='blue', dash='solid')))

fig.add_trace(go.Scatter(x=ts, y=R_s, name=r'$R_{ij}(t)$', line=dict(color='magenta', dash='solid')))

fig.add_trace(go.Scatter(x=ts, y=rho_s, name=r'$\rho(t)$', line=dict(color='purple', dash='solid')))

fig.update_layout(
    yaxis_range=[-3,3],
                  xaxis_range=[0,1],
                  width=700,
                  height=700,
                  showlegend=True,
                  title_text=r'$\text{Radiation-Dominated FLRW Evolution}$',
                  title_x=0.5,
                  xaxis_title=r'$t/t_o$',
                  yaxis_title=r'$\text{Cosmological Quantity}$')
fig.show()
```

```python
# fig.write_image('/Users/jim/repos/tex/homework/courses/PHYS510 - GR I/hw6/figures/fig-b6.pdf')
```

### B7

```python
ts = numpy.arange(0.001, 2.001, 0.001)
a_s = ts ** (2.0 / 3.0)
H_s = 2 / (3 * ts)
R_s =  - 2 / (9 * ts ** 2)
rho_s = 1 / (2 * numpy.pi * ts ** 2)
```

```python
fig = go.Figure()

fig.add_trace(go.Scatter(x=ts, y=a_s, name=r'$a(t)$', line=dict(color='green', dash='solid')))

fig.add_trace(go.Scatter(x=ts, y=H_s, name=r'$H(t)$', line=dict(color='blue', dash='solid')))

fig.add_trace(go.Scatter(x=ts, y=R_s, name=r'$R_{ij}(t)$', line=dict(color='magenta', dash='solid')))

fig.add_trace(go.Scatter(x=ts, y=rho_s, name=r'$\rho(t)$', line=dict(color='purple', dash='solid')))

fig.update_layout(
    yaxis_range=[-3,3],
                  xaxis_range=[0,1],
                  width=700,
                  height=700,
                  showlegend=True,
                  title_text=r'$\text{Matter-Dominated FLRW Evolution}$',
                  title_x=0.5,
                  xaxis_title=r'$t/t_o$',
                  yaxis_title=r'$\text{Cosmological Quantity}$')
fig.show()
```

```python
fig.write_image('/Users/jim/repos/tex/homework/courses/PHYS510 - GR I/hw6/figures/fig-b7.pdf')
```

### B8

```python
G, a_o, Lam, rho_o = sympy.symbols('G a_o Lambda rho_o')
```

```python
a_b8 = a_o * (sympy.sqrt(8 * sympy.pi * G * rho_o / Lam) *
              sympy.sinh(sympy.sqrt(3 * Lam) * t / 2)) ** (sympy.Rational(2, 3))
```

```python
a_b8
```

```python
a_dot_b8 = sympy.simplify(sympy.Derivative(a_b8, t).doit())
```

```python
a_dot_b8
```

```python
hubble_b8 = sympy.simplify(a_dot_b8 / a_b8)
```

```python
hubble_b8
```

```python
a_ddot_b8 = sympy.Derivative(a_dot_b8, t).doit()
```

```python
R_ij_b8 = sympy.simplify(a_ddot_b8 / a_b8)
```

```python
R_ij_b8
```

```python
rho_b8 = sympy.simplify(rho_o * (a_o / a_b8) ** 3)
```

```python
rho_b8
```

```python
lhs = sympy.simplify(a_dot_b8 ** 2)
lhs
```

```python
rhs = sympy.Rational(1, 3) * Lam * a_b8 ** 2 + sympy.Rational(8, 3) * sympy.pi * G * rho_o * (a_o ** 3) / a_b8
rhs = sympy.simplify(rhs)
rhs
```

```python
print(R_ij_b8._repr_latex_())
```

```python
sympy.simplify(lhs - rhs)
```

```python
rho_b8._
```

#### Visual

```python
a_b8
```

```python
a_b8_callable = sympy.lambdify((t, G, rho_o, Lam, a_o), a_b8)
H_b8_callable = sympy.lambdify((t, G, rho_o, Lam, a_o), hubble_b8)
R_b8_callable = sympy.lambdify((t, G, rho_o, Lam, a_o), R_ij_b8)
rho_b8_callable = sympy.lambdify((t, G, rho_o, Lam, a_o), rho_b8)
```

```python
a_b8_res = a_b8_callable(ts, 1, 1, 1, 1)
H_b8_res = H_b8_callable(ts, 1, 1, 1, 1)
R_b8_res = R_b8_callable(ts, 1, 1, 1, 1)
rho_b8_res = rho_b8_callable(ts, 1, 1, 1, 1)
```

```python
fig = go.Figure()

fig.add_trace(go.Scatter(x=ts, y=a_b8_res, name=r'$a(t)$', line=dict(color='green', dash='solid')))

fig.add_trace(go.Scatter(x=ts, y=H_b8_res, name=r'$H(t)$', line=dict(color='blue', dash='solid')))

fig.add_trace(go.Scatter(x=ts, y=R_b8_res, name=r'$R_{ij}(t)$', line=dict(color='magenta', dash='solid')))

fig.add_trace(go.Scatter(x=ts, y=rho_b8_res, name=r'$\rho(t)$', line=dict(color='purple', dash='solid')))

fig.update_layout(
    yaxis_range=[-3,3],
                  xaxis_range=[0,1],
                  width=700,
                  height=700,
                  showlegend=True,
                  title_text=r'$\text{Matter + }\Lambda\text{ Dominated FLRW Evolution}$',
                  title_x=0.5,
                  xaxis_title=r'$t/t_o$',
                  yaxis_title=r'$\text{Cosmological Quantity}$')
fig.show()
```

```python
fig.write_image('/Users/jim/repos/tex/homework/courses/PHYS510 - GR I/hw6/figures/fig-b8.pdf')
```

```python

```

## Problem C

### C1

```python
H = 70e3 * (units.m / (units.s * units.Mpc) )
H
```

```python
rho_crit = (3 / (8 * numpy.pi * constants.G)) * H ** 2
rho_crit
```

```python
rho_crit_per_proton = rho_crit / constants.m_p
rho_crit_per_proton
```

```python
rho_crit_per_proton.to((1 / units.m) ** 3)
```

```python
constants.G
```

### C2

```python
(1 / H).to(units.yr)
```

### C3

```python
omega_m = 0.31
```

```python
rho_crit = (3 / (8 * numpy.pi * constants.G)) * H ** 2
rho_crit = rho_crit.to(units.kg / units.m ** 3)
rho_init = omega_m * rho_crit
```

```python

sinh_arg = numpy.sqrt(rho_crit / rho_init * (1 - omega_m)).value

factor = 1 / numpy.sqrt(6 * numpy.pi * G * rho_crit * (1 - omega_m))
t_o = factor * numpy.arcsinh(sinh_arg)
```

```python
t_o.to(units.yr)
```

### C4

```python
omega_lam = 0.69
Lam_c4 = 8 * numpy.pi * constants.G * rho_crit * omega_lam
Lam_c4
```

```python
t_star = 2 * numpy.log(1 + numpy.sqrt(2)) / numpy.sqrt(3 * Lam_c4)
t_star
```

```python
t_star.to(units.yr)
```

```python
t_o.to(units.yr) - t_star.to(units.yr)
```

```python

```


```
%matplotlib inline
from IPython.html.widgets import interact
from scipy import  stats
import seaborn as sns
import pandas as pd
```

Previously, we talk about maximum likelihood estimation and maximum a-posteriori estimation and in each case we started out with a probability density function of some kind and we further assumed that the samples were identically distributed and independent. The idea behind robust statistics is to construct estimators that can survive the weakening of either or both of these assumptions.

The first idea to consider is the notion of *location*, which is  a generalization of the idea of "central value". Typically, we just use an estimate of the mean for this, but we will see shortly why that is a bad idea.  The general idea of Location satisfies the following requirements

Let $ X $ be a random variable with distribution $ F $, and let $\theta(X)$ be some descriptive
measure of $F$. Then $\theta(X)$ is said to be a measure of *location* if for any constants *a* and *b*, we have the following:

$$ \theta(X+b) = \theta(X) +b $$

$$ \theta(-X) = -\theta(X)$$

$$ X \ge 0 \Rightarrow \theta(X)  \ge 0 $$

$$ \theta(a X) = a\theta(X) $$

The first condition is called *location equivariance* (or *shift-invariance*  in signal processing lingo). The fourth condition is called *scale equivariance*, which means that the units that $X$ is measured in should not effect the value of the  location estimator.  These Requirements capture the idea of what we intuitively mean by *centrality* of a distribution, or where most of the probability mass is located.

For example, the mean estimator is $ \hat{\mu}=\frac{1}{n}\sum X_i $. The first requirement is obviously satisfied as $ \hat{\mu}=\frac{1}{n}\sum (X_i+b) = b +  \frac{1}{n}\sum X_i =b+\hat{\mu}$. Let us consider the second requirement:$ \hat{\mu}=\frac{1}{n}\sum -X_i = -\hat{\mu}$. Finally, the last requirement is satisfied with $ \hat{\mu}=\frac{1}{n}\sum a X_i =a \hat{\mu}$.

## What do we mean by robust estimators?

Now that we have the generalized location of centrality embodied in the *location* parameter, what can we do with it?  The next idea is to nail down is the concept of * robust* estimators. Previously, we assumed that our samples were all identically distributed. The key idea is that the samples might be actually coming from a distribution that is contaminated by another nearby distribution, as in the following:

$$ F(X) = \epsilon G(X) + (1-\epsilon)H(X) $$

where $ \epsilon $ is between zero and one. This means that our data samples $\lbrace X_i \rbrace$ actually derived from two separate distributions, $ G(X) $ and $ H(X) $. We just don't know how they are mixed together. What we really want  is an estimator  that captures the location of $ G(X) $ in the face of random intermittent contamination by $ H(X) $. It can get even worse than that because we don't know that there is only one contaminating $H(X)$ distribution out there. There may be a whole family of distributions that are contaminating $G(X)$ that we don't know of. This means that whatever estimators we construct have to be derived from families of distributions instead of a distribution, which is what we have been assuming for maximum-likelihood  estimators. This is what makes robust estimation so difficult --- the extended theory has to deal with spaces of function distributions instead of particular parameters of a particular probability distribution.

* Influence function
* Outlier Detection
* Estimates of location
    - definition of location
* Trimmed means
* Windsorized means
* Hodges Lehmann statistics
* Asymptotic efficiency
* Fisher Consistent

* Robust Regression

    - least median
    - outliers

```
n0=stats.norm(0,1)
n1=stats.norm(0,10)
xi = linspace(-5,5,100)

fig,ax=subplots()
ax.plot(xi,n0.pdf(xi))
ax.plot(xi,n1.pdf(xi))

def bias_coin(phead = .5):
    while True:
        yield int( np.random.rand() < phead )

pct_mixed  = 0.1
bias_coin_gen = bias_coin(pct_mixed)
dual_set = [n0,n1]
samples  = [ dual_set[bias_coin_gen.next()].rvs() for i in range(500) ]
```

```
hist(samples,bins=20)
title('average = %3.3f, median=%3.3f pct_mixed=%3.3f'%(mean(samples),np.median(samples),pct_mixed))
```

```
import sympy.stats
from sympy.abc import x
eps = sympy.symbols('epsilon')

mixed_cdf = sympy.stats.cdf(sympy.stats.Normal('x',0,1),'x')(x)*(1-eps) + eps*sympy.stats.cdf(sympy.stats.Normal('x',1,2),'x')(x)
mixed_pdf = sympy.diff(mixed_cdf,x)

```

```
def plot_mixed_dist(epsilon=.1):
    n1 = stats.norm(1,2)
    xi = linspace(-5,5,100)
    fig,ax = subplots()
    ax.plot(xi,[sympy.lambdify(x,mixed_pdf.subs(eps,epsilon))(i) for i in xi],label='mixed',lw=2)
    ax.plot(xi,n0.pdf(xi),label='g(x)',linestyle='--')
    ax.plot(xi,n1.pdf(xi),label='h(x)',linestyle='--')
    ax.legend(loc=0)
    ax.set_title('epsilon = %2.2f'%(epsilon))
    ax.vlines(0,0,.4,linestyle='-',color='g')
    ax.vlines(epsilon,0,.4,linestyle='-',color='b')

interact(plot_mixed_dist,epsilon=(0,1,.05))
```

## M-estimators

M-estimators are generalized maximum likelihood estimators. Recall that for maximum likelihood, we want to maximize the likelihood function as in the following:

$$ L_{\mu}(x_i) = \prod f_0(x_i-\mu)$$

and then to find the estimator $\hat{\mu}$ so that

$$ \hat{\mu} = \arg \max_{\mu} L_{\mu}(x_i) $$

So far, everything is the same as our usual maximum-likelihood  derivation except for the fact that we don't know $f_0$, the distribution of the $\lbrace X_i\rbrace$. Making the convenient definition of

$$ \rho = -\log f_0 $$

we obtain the more convenient form of the likelihood product and the optimal $\hat{\mu}$ as

$$ \hat{\mu} = \arg \min_{\mu} \sum \rho(x_i-\mu)$$

If $\rho$ is differentiable, then differentiating  this with respect to $\mu$ gives

$$ \sum \psi(x_i-\hat{\mu}) = 0 $$

with $\psi = \rho'$ and for technical reasons we will assume that $\psi$ is increasing. The key idea here is we want to consider general $\rho$ functions that my not be MLE for *any* distribution.

### The distribution of  M-estimates

For a given distribution $F$, we define $\mu_0=\mu(F)$ as the solution to the following

$$ \mathbb{E}_F(\psi(x-\mu_0))= 0 $$

It is technical to show, but it turns out that $\hat{\mu} \sim \mathcal{N}(\mu_0,\frac{v}{n})$ with

$$ v = \frac{\mathbb{E}_F(\psi(x-\mu_0)^2)}{(\mathbb{E}_F(\psi^\prime(x-\mu_0)))^2} $$

Thus, we can say that $\hat{\mu}$ is asymptotically normal with asymptotic value $\mu_0$ and asymptotic variance $v$. This leads to the efficiency ratio which is defined as  the following:

$$ \texttt{Eff}(\hat{\mu})= \frac{v_0}{v} $$

where $v_0$ is the asymptotic variance of the MLE and measures how near $\hat{\mu}$ is to the optimum. for example, if for two estimates with asymptotic variances $v_1$ and $v_2$, we have $v_1=3v_2$, then first estimate requires three times as many observations to obtain the same variance as the second.

For example, for the sample mean (i.e. $\hat{\mu}=\frac{1}{n} \sum X_i$) with $F=\mathcal{N}$, we have $\rho=x^2/2$ and $\psi=x$ and also $\psi'=1$. Thus, we have $v=\mathbb{V}(x)$. Alternatively, using the sample median as the estimator for the location, we have $v=\frac{1}{4 f(\mu_0)^2}$. Thus, if we have $F=\mathcal{N}(0,1)$, for the sample median, we obtain $v=\frac{2\pi}{4} \approx 1.571$. This means that the sample median takes approximately 1.6 times as many samples to obtain the same variance for the location as the sample mean.

One way to think about M-estimates is a weighted means. Most of the time, we have $\psi(0)=0$ and $\psi'(0)$ exists so that $\psi$ is approximately linear at the origin. Using the following definition:

$$ W(x)  =  \begin{cases}
                \psi(x)/x & \text{if} \: x \neq 0 \\
                \psi'(x)  & \text{if} \: x =0
            \end{cases}
$$

We can write our earlier equation as follows:

$$ \sum W(x_i-\hat{\mu})(x_i-\hat{\mu}) = 0 $$

Solving this for $\hat{\mu} $ yields the following,

$$ \hat{\mu} = \frac{\sum w_{i} x_i}{\sum w_{i}} $$

where $w_{i}=W(x_i-\hat{\mu})$. The question that remains is how to pick the $\psi$ functions.

### Huber functions

The family of Huber function is defined by the following:

$$ \rho_k(x ) = \begin{cases}
                x^2  & \text{if} \: |x|\le k \\
                2 k |x|-k^2 & \text{if} \: |x| \gt k
                \end{cases}
$$

with corresponding derivatives $2\psi_k(x)$ with

$$ \psi_k(x ) = \begin{cases}
                x  & \text{if} \: |x|\le k \\
                \text{sgn}(x)k & \text{if} \: |x| \gt k
                \end{cases}
$$
where the limiting cases $k \rightarrow \infty$ and $k \rightarrow 0$ correspond to the mean and median, respectively. To see this, take $\psi_{\infty} = x$ and therefore $W(x) = 1$ and thus the defining equation results in

$$ \sum_{i=1}^{n} (x_i-\hat{\mu}) = 0 $$

and then solving this leads to $\hat{\mu} = \frac{1}{n}\sum x_i$. Note that choosing $k=0$ leads to  the sample median, but that is not so straightforward to solve for.

```
fig,ax=subplots()
colors=['b','r']
for k in [1,2]:
    ax.plot(xi,np.ma.masked_array(xi,abs(xi)>k),color=colors[k-1])
    ax.plot(xi,np.ma.masked_array(np.sign(xi)*k,abs(xi)<k),color=colors[k-1],label='k=%d'%k)
ax.axis(ymax=2.3,ymin=-2.3)
ax.set_ylabel(r'$\psi(x)$',fontsize=28)
ax.set_xlabel(r'$x$',fontsize=24)
ax.legend(loc='best')
ax.set_title('Huber functions')
ax.grid()
```

The $W$ function corresponding to Huber's $\psi$ is the following:

$$ W_k(x) = \min\Big{\lbrace} 1, \frac{k}{|x|} \Big{\rbrace} $$

which is plotted in the following cell for a few values of $k$.

```
fig,ax=subplots()
ax.plot(xi,np.vstack([np.ones(xi.shape),2/abs(xi)]).min(axis=0),label='k=2')
ax.plot(xi,np.vstack([np.ones(xi.shape),1/abs(xi)]).min(axis=0),label='k=1')
ax.axis(ymax=1.1)
ax.legend(loc=0)
ax.set_title("Huber's weight function")
```

Another alternative intuitive way  to interpret  the M-estimate is to rewrite the following:

$$ \hat{\mu} = \hat{\mu} +\frac{1}{n}\sum_i \psi(x_i-\hat{\mu}) = \frac{1}{n} \sum_i \zeta(x_i,\hat{\mu})$$

which for the Huber family of functions takes on the form :

$$ \zeta(x,\mu) = \begin{cases}
                    \mu - k & \text{if} \: x \lt \mu-k \\
                    x & \text{if} \: \mu-k \le x \le \mu+k \\
                    \mu+k & \text{if} \: x \gt \mu \\
                  \end{cases}
$$

Thus, the interpretation here is that $\hat{\mu}$ is the average of the truncated pseudo-observations $\zeta_i$ where the observations beyond a certain point are clipped at the $k$-offset of the $\hat{\mu}$.

## Asymptotic variance

```
from sympy import mpmath, symbols, diff, Piecewise, sign, lambdify
from sympy.stats import density, cdf, Normal
from sympy.abc import k,x
```

```
eps = symbols('epsilon')
lpdf=diff(cdf(Normal('x',0,1))(x)*(1-eps)+ eps*cdf(Normal('x',0,10))(x),x)
p = Piecewise((x,abs(x)<k),(k*sign(x),True))
```

```
def closure_on_asymptotic_variance(mn=(0,0),std=(1,10)):
    from sympy.abc import k,x
    eps = symbols('epsilon')
    lpdf=diff(cdf(Normal('x',mn[0],std[0]))(x)*(1-eps)+ eps*cdf(Normal('x',mn[1],std[1]))(x),x)
    def asymptotic_variance(kval,epsval):
        p = Piecewise((x,abs(x)<kval),(kval*sign(x),True))
        denom=mpmath.quad(lambdify(x,lpdf.subs(eps,epsval)),[-kval,kval])**2
        numer=mpmath.quad(lambdify(x,(p.subs(k,kval))**2*lpdf.subs(eps,epsval)),[-np.inf,np.inf])
        return float(numer/denom)
    return asymptotic_variance
```

```
case2=closure_on_asymptotic_variance()
```

```
kvals= [.001,0.3,0.5,.7,1.00001,1.4,1.7,2,3,4,5]
```

```
fig2,ax=subplots()
ax.plot(kvals,[case2(k,0) for k in kvals],'-o',label='eps=0')
ax.plot(kvals,[case2(k,.05) for k in kvals],'-o',label='eps=.05')
ax.plot(kvals,[case2(k,.1) for k in kvals],'-o',label='eps=0.1')
ax.set_xlabel("k")
ax.set_ylabel("asymptotic variance")
ax.legend(loc=0)
ax.set_title(r"$\mathcal{N}(0,1) , \mathcal{N}(0,10)$ mixed",fontsize=18)
```

## Computable Example

```
nsamples = 500
ncols = 300
xs = np.array([dual_set[bias_coin_gen.next()].rvs() for i in range(ncols)*nsamples ]).reshape(nsamples,-1)
```

```
fig,ax=subplots()
ax.hist(np.mean(xs,0),20,alpha=0.8,label = 'mean')
ax.hist(np.median(xs,0),20,alpha=0.3,label ='median')
ax.legend()
```

```
fig,ax=subplots()
sns.violinplot(np.vstack([np.median(xs,axis=0),np.mean(xs,axis=0)]).T,ax=ax,names=['median','mean']);
```

```
def huber_estim(x,k=1,tol=1e-6):
    if x.ndim==2: # loop over columns
        out = []
        for i in range(x.shape[1]):
            out.append(huber_estim(x[:,i],k=k)) # recurse
        return np.array(out)
    else:
        mu = median(x)
        mad = median(abs(x-mu))*1.4826 # follow MADN convention
        while True:
            mu_i=mean(minimum(maximum(mu-k*mad,x),mu+k*mad))
            if abs(mu-mu_i) < tol*mad: break
            mu = mu_i
        return mu_i
```

```
huber_est={k:huber_estim(xs,k) for k in [1,1.5,2,3]}
huber_est[0] = np.median(xs,axis=0)
huber_est[4] = np.mean(xs,axis=0)
fig,ax=subplots()
sns.violinplot(pd.DataFrame(huber_est),ax=ax)
ax.set_xticklabels(['median','1','1.5','2','3','mean']);
```

```
huber_est={k:huber_estim(xs,k) for k in kvals}
huber_est_df = pd.DataFrame(huber_est)
```

```
fig,ax=subplots()
sns.violinplot(huber_est_df,ax=ax)
```

```
fig2,ax=subplots()
ax.plot(kvals,[case2(k,pct_mixed) for k in kvals],'-o',label='eps=0.1')
(huber_est_df.var()*nsamples).plot(marker='o',label='est eps=0.1',ax=ax)
ax.set_xlabel("k")
ax.set_ylabel("asymptotic variance")
ax.legend(loc=0)
```

## References

* Maronna, R. A., R. D. Martin, and V. J. Yohai. "Robust Statistics: Theory and Methods". 2006.


# **Tensile behavior of brittle-matrix composite**

[](https://moodle.rwth-aachen.de/mod/page/view.php?id=602450)

With a basic understanding of the debonding process in the vicinity of a crack bridge studied for various configurations of a pull-out problem ([2.2 Classification of pull-out configurations](2_2_1_PO_configuration_explorer.ipynb)) we describe and visualize the process of fragmentation (multiple cracking) in a reinforced composite.

Consider an example the textile reinforced concrete cross section loaded in tension. The cross-sectional parameters are as follows

# Crack bridge as a key to understanding the behavior of a composite

Matrix cracks develop along a tensile zone of a composite, e.g. in a tensile specimen.
The stress, strain and displacement fields exhibit a periodic structure with symmetry points
at
 - crack positions, and at
 - midpoints between cracks.

Therefore, it the crack bridge model derived for the assumption of a constant bond-slip law can be
conveniently used to describe the tensile response during the multiple cracking process.

## What do we need from a crack bridge model?

```python
%matplotlib widget
from pull_out import CB_ELF_ELM
po = CB_ELF_ELM(E_f=1, E_m=1, A_f=1, A_m=1, p=1, tau=3, L_b=1, w_max=3,t=0.4)
po.interact()
```

    VBox(children=(HBox(children=(VBox(children=(Tree(layout=Layout(align_items='stretch', border='solid 1px black…

Knowing the matrix stress profile ahead of an existing crack, we know the distance at which no crack will appear. This length is referred to as shielded length.

### **Question:** How long is the shielded length?

Use the crack bridge model to find out the distance $l_\mathrm{shielded}$ at which the matrix normal force attains the level
\begin{align}
N_\mathrm{mu} & = \sigma_\mathrm{mu} A_\mathrm{m}
\end{align}
Realizing that the slope at which the matrix stress grows is
\begin{align}
T &= p \bar{\tau}
\end{align}
As a result, the distance can be evaluated as
\begin{align}
l_\mathrm{shielded} = \dfrac{N_\mathrm{mu}}{T}
\end{align}

### **Question:** What is the elongation and average strain of a crack segment?

A simple way how to find out the total elongation of the fiber with a nonlinear strain profile is to integrate the strains, i.e.
\begin{align}
\Delta u_\mathrm{f} = \int_{-L_\mathrm{b}}^0 \varepsilon_\mathrm{f} \, \mathrm{d}x
\end{align}

so that the average, or **composite** strain is obtained as

\begin{align}
\varepsilon_\mathrm{c} = \dfrac{1}{L_\mathrm{b}} \int_{-L_\mathrm{b}}^0 \varepsilon_\mathrm{f} \, \mathrm{d}x
\end{align}

**Remark:** This expression is important in view of homogenization of non-uniform strain profiles in a heterogenous material structure. It is one of the fundamental concepts of micromechanics. We will use it later on to derive the effective stress-strain curve of a multiply cracked composite.

# **Model 1:** Deterministic matrix strength (ACK model)

The typical shape of the stress-strain curve of the composite involves three stages:
- elastic stage which is governed by the mixture rule
- stage of matrix fragmentation
- saturated crack pattern with a linear branch

How to interpret and characterize these three distinguished phases of composite material behavior?

The ACK model developed by Aveston, Cooper and Kelly is an analytical model that represents the composite tensile response by a trilinear law as shown in the following figure. This model is based on the following assumptions:
- The bond behavior is governed by a constant frictional bond in the debonded interface
- The constitutive law for both reinforcement and matrix is assumed to be linear-elastic with brittle failure upon reaching their strengths
- Multiple cracking occurs at a constant level of applied stress, inducing a horizontal branch in the stress-strain behavior

## Stress-strain curve

The trilinear curve of ACK model represents the composite tensile response by identifying the following characteristic points:
- [$\sigma_{1}, \varepsilon_{1}$]: The inital values of stress and strain are set to zero.
- [$\sigma_{2}, \varepsilon_{2}$]: In the first stage, the matrix is uncracked and perfect bond between matrix and fabric is assumed up to the first cracking stress $\sigma_{2}$ , which is defined as
\begin{align}
\sigma_{2} = E_\mathrm{c} \varepsilon_{2}
\end{align}
where $\varepsilon_{2}$ is the composite strain value at which the matrix cracks and $E_\mathrm{c}$ is the composite stiffness.
The strain $\varepsilon_{2}$ is given as
\begin{align}
\varepsilon_{2} = \dfrac{\sigma_\mathrm{mu}}{E_\mathrm{m}}
\end{align}
where $\sigma_\mathrm{mu}$ and $E_\mathrm{m}$ are the matrix tensile strength and stiffness, respectively.
The composite stiffness has been obtained using the
[**mixture rule**](../bmcs_course/1_1_elastic_stiffness_of_the_composite.ipynb)
and can be expressed here as:
\begin{align}
E_\mathrm{c} = E_\mathrm{f} \; V_\mathrm{f} + E_\mathrm{m} \; (1 - V_\mathrm{f})
\end{align}
where $E_\mathrm{f}$ is the fiber stiffness, and $V_\mathrm{f}$ is denoting the fiber volume fraction (reinforcement ratio).

- [$\sigma_{3}, \varepsilon_{3}$]: The second stage is characterized by the crack propagation. In this phase, the load is
assumed to be constant up to the strain value $\varepsilon_{3}$ calculated as follows:
\begin{align}
\varepsilon_{3} = \dfrac{\sigma_\mathrm{mu}}{E_\mathrm{m}} \; (1 + 0.666  \alpha_\mathrm{e})
\end{align}
where where $\alpha_\mathrm{e}$ is an homogenization coefficient given as
\begin{align}
\alpha_\mathrm{e} = \dfrac{E_\mathrm{m} \; (1 - V_\mathrm{f}) }{E_\mathrm{f} \; V_\mathrm{f}}
\end{align}

- [$\sigma_{4}, \varepsilon_{4}$]: Finally, in the third stage, when the crack pattern is stabilized the load increases linearly up to the ultimate tensile stress $\sigma_4$  with a slope equal to $E_\mathrm{r}$
The ultimate tensile stress is given as
\begin{align}
\sigma_4 = \sigma_\mathrm{fu} \; V_\mathrm{f}
\end{align}
where $\sigma_\mathrm{fu}$ is the tensile strength of the fiber.
The slope $E_\mathrm{r}$ represents the effective stiffness of the reinforcement with respect to the whole cross section and is given as
\begin{align}
E_\mathrm{r} = E_\mathrm{f} \; V_\mathrm{f}
\end{align}
The composilte strain at failure $\varepsilon_{4}$ is given as
\begin{align}
\varepsilon_{4} = \varepsilon_{3} + \dfrac{\sigma_\mathrm{4} - \sigma_\mathrm{2}}{E_\mathrm{r}}
\end{align}

## Crack spacing

### Matrix cracking is like car parking ?!

Consider a process where particles (cars) are randomly introduced in a system (along the street).
They must not overlap any previously parked car. What is the average distance between two neighbouring cars?

Probabilistic analysis of the car parking problem delivers the result that the average spacing is 1.337 larger than the car length ([Wikipedia: Random sequential adsorption](https://en.wikipedia.org/wiki/Random_sequential_adsorption))

### How long is the car in a concrete tensile specimen?

The final average crack spacing $l_\mathrm{cs}$ is given as

\begin{align}
l_\mathrm{cs} &= 1.337 \; l_\mathrm{shielded} =  1.337 \; \dfrac{A_\mathrm{m} \sigma_\mathrm{mu}}{\bar{\tau}p}
\end{align}

## Examples

### **Task 1:** Evaluate the tensile stress-strain curve
Consider a steel reinforced cross section of $100 \times 100$ mm
reinforced with a rebar ($d = 16$ mm) diameter.
Plot the stress-strain curve using the ACK model:

```python
E_m = 25000 # 28000 # concrete stiffness [MPa]
E_f = 182000 # 210000 # reinforcement stiffnes [MPa]
A_c = 100 * 100 # dimensions [mm]
d = 16 # diameter [mm]
sig_mu = 3 # 3 # concrete tensile strength [MPa]
sig_fu = 1380 # 500 # reinforcement strength [MPa]
```

Derived parameters of the cross composite section

```python
p = 3.14 * d # bond perimeter
A_f = 3.14 * (d/2)**2 # reinforcement cross section
A_m = A_c - A_f # concrete cross section
V_f = 0.01 # A_f / A_c # reinforcement ratio / fiber volume fraction
E_c = E_m * (1 - V_f) + E_f * V_f # composite stiffness
alpha_e = E_m * (1 - V_f) / (E_f * V_f) # homogenization coefficient
```

```python
E_c
```

    26570.0

```python
V_f
```

    0.01

Characteristic points of the ACK model deliver the values

```python
eps_1, sig_1 = 0, 0
eps_2 = sig_mu / E_m
sig_2 = eps_2 * E_c
eps_3, sig_3 = sig_mu / E_m * (1 + 0.6666 * alpha_e), sig_2
sig_4 = sig_fu * V_f
E_r = E_f * V_f # effective reinforcement stiffness related to the composite cross section
eps_4 = eps_3 + (sig_4 - sig_3) / E_r
```

```python
sig_2, sig_4
```

    (3.1884, 13.8)

Plot the composite stress-strain curve

```python
%matplotlib widget
import matplotlib.pylab as plt
fig, ax = plt.subplots(1,1,figsize=(7,3))
fig.canvas.toolbar_position = 'top'
fig.canvas.header_visible = False
ax.plot([eps_1, eps_2, eps_3, eps_4], [sig_1, sig_2, sig_3, sig_4]);
ax.set_xlabel(r'$\varepsilon$ [-]'); ax.set_ylabel(r'$\sigma$ [MPa]')
ax.plot([0,eps_4],[0,E_r*eps_4], color='black', linewidth=1, linestyle='dashed');
```

    Canvas(toolbar=Toolbar(toolitems=[('Home', 'Reset original view', 'home', 'home'), ('Back', 'Back to previous …

**Note** that the curve does not depend on the bond $\tau p$.

### **Task 2:** Evaluate the crack spacing

What does ACK model predict for a specimen with the dimensions $100 \times 100$ mm reinforced with 1% ratio.

```python
A_c = 100 * 100
d = 16
p = 3.14 * d
A_f = 3.14 * (d/2)**2
A_m = A_c - A_f
tau = 8
1.337 * A_m * sig_mu / (p * tau)
```

    97.79047929936306

which corresponds to reinforcement ratio $V_f$

```python
A_f / A_c
```

    0.020096

# **Model 2:** Random matrix strength

In reality, the matrix strength $\sigma_\mathrm{mu}$ is random. Its profile along the tensile specimen can be described by the probability distribution function.
Weibull probability distribution is used to describe the strength of materials.

```python
%matplotlib widget
from pmcm import PMCM
pm = PMCM()
pm.interact()
```

    VBox(children=(HBox(children=(VBox(children=(Tree(layout=Layout(align_items='stretch', border='solid 1px black…

The probabilistic multiple cracking model uses the crack bridge model which is inserted at any newly emerging crack position. By assembling the profile along the specimen we obtain the
 - matrix stress field  $\sigma_\mathrm{m}(x, \sigma_\mathrm{c})$, and
 - reinforcement strain field $\varepsilon_\mathrm{f}(x, \sigma_\mathrm{c})$.
for any state of loading $\sigma_\mathrm{c}$. No finite element calculation is needed.

The algorithm used in the above web-app can identify the individual cracks exactly. For a given load level $\sigma_\mathrm{c}$ and crack distribution along the specimen, the composite strain of a specimen with a length $L$ is obtained using the averaging formula introduced above
\begin{align}
\varepsilon_\mathrm{c} = \dfrac{1}{L} \int_{0}^L \varepsilon_\mathrm{f} \, \mathrm{d}x
\end{align}

**Further reading:** [Paper describing the general probabilistic multiple cracking model](../papers/pmcm_fragmentation.pdf), Journal of Mathematical Modeling (2021)

## PMCM application

Apply the model to a composite cross section using carbon textile fabrics as specified in the notebook [**Mixture rule**](1_1_elastic_stiffness_of_the_composite.ipynb)

```python
A_roving = 0.5 # [mm**2]
n_layers = 6 # -
spacing = 8.3 # [mm]
thickness = 10 # [mm]
width = 100 # [mm]
E_carbon = 160000 # [MPa]
E_concrete = 28000 # [MPa]
```

```python
A_composite = width * thickness
n_rovings = width / spacing
A_layer = n_rovings * A_roving
A_carbon = n_layers * A_layer
A_concrete = A_composite - A_carbon
E_composite = (E_carbon * A_carbon + E_concrete * A_concrete) / A_composite
E_composite
```

    32771.0843373494

```python
pm2 = PMCM(sig_cu=20, sig_mu=4, m=8)
pm2.crack_bridge.trait_set(E_m=E_concrete, E_f=E_carbon, tau=8,
                          A_c=A_composite, A_f=A_carbon, p=n_rovings*n_layers*3.14)
pm2.interact()
```

    VBox(children=(HBox(children=(VBox(children=(Tree(layout=Layout(align_items='stretch', border='solid 1px black…

## Exercises

### Task 3: Use the model to validate its prediction
Given the cross sectional areas $A_\mathrm{m}, A_\mathrm{f}$, concrete and reinforcement stiffness $E_\mathrm{m}, E_\mathrm{f}$ and strength $\sigma_\mathrm{mu}, \sigma_\mathrm{fu}$, reinforcement ratio $V_\mathrm{f}$, bond stress $\tau$, and perimeter $p$ calculate the average crack width at failure of a tensile specimen.
### Task 4: Evaluate the average crack width
Compare the crack width obtained using the ACK model and the PMCM model for a given reinforced cross section design

        Exercise X0203: Tensile behavior of a composite with constant bond-slip law

      2.2 Classification of pullout configurations

```python

```