```python
import numpy as np
```

# 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}& = & w h_2 l_2 \\
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 &=& w h_1 l_1 \\
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).

We start by defining the given functions:

```python
import matplotlib.pyplot as plt
```

```python
def consumption(w, h, l):
    """Args:
    w : wage
    h : human capital
    l : working dummy

    Returns:
    The worker's consumption"""

    #We define an outcome for each scenario
    if l == 1:
        return w*h*l
    else:
        return b
```

```python
def utility(w, h, l):
    """Args:
    w : wage
    h : human capital
    l : working dummy

    Returns:
    The worker's utility"""
    return (consumption(w,h,l)**(1-rho))/(1-rho) - gamma*l
```

We create the 2 possible values for human capital in period 2.

```python
h2_vec0 = h_vec + Delta*0.5
h2_vec1 = h_vec + 1 + Delta*0.5
```

First of all we notice that she can reach the highest utility in period 2 if she choose to work in period 1. This can be seen in the graph below.

```python
#Making a list which only contains the utility if she does not work in period 2. We only do this to be able to plot it.
empty = np.empty(100)
utility_l0 = []
for i in empty:
    utility_l0.append(utility(w,h2_vec0,0))

plt.plot(h2_vec0, utility(w,h2_vec0,1), label = 'If $l_2=1$ and $l_1=0$')
plt.plot(h2_vec0, utility(w,h2_vec1,1), label = 'If $l_2=1$ and $l_1=1$')
plt.plot(h2_vec0, utility_l0          , label = 'If $l_2=0$')
plt.ylabel('Utility ($v_2(h_2)$)'), plt.xlabel('Human Capital in period 2')
plt.legend()
plt.grid()
plt.show()
```

The most important thing to notice here is, that she can actually get higher utility by not working in period 2, than if she doesn't work in period 1. This depends on how high her human capital is.

We now solve for the optimal $l_2$:

```python
def solve_v2(w):
    """Args:
    w : wage

    Returns:
    2 vectors indicating if the worker wants to work or not as a function of human capital"""
    loop = np.linspace(0,99,100,dtype=int)
    #We make 2 lists which will contain the values 0 or 1 depending on if she gains more utility of working or not
    #First a lists where she does not work in period 1:
    l2_vec0 = []
    #Then a list where she does work in period 1:
    l2_vec1 = []

    #Making the for loop to generate values 0 or 1 into our lists
    for i in loop:

        #If she does not work in period 1:
        if utility(w, h2_vec0[0], 0) < utility(w, h2_vec0[i], 1):
            l2_vec0.append(1)
        else:
            l2_vec0.append(0)

        #If she works in period 1:
        if utility(w, h2_vec1[0], 0) < utility(w, h2_vec1[i], 1):
            l2_vec1.append(1)
        else:
            l2_vec1.append(0)

    return l2_vec0, l2_vec1

l2_vec0, l2_vec1 = solve_v2(w)
```

We plot the solutions to inspect them:

```python
plt.figure(figsize=(10,5))
plt.subplot(121)
plt.title('If $l_1 = 0$')
plt.plot(h2_vec0, l2_vec0)
plt.grid()
plt.ylabel('Labor supply')
plt.xlabel('Human capital in period 2')
plt.xticks([0,0.2,0.4,0.6,0.8,1,1.2,1.4,1.6])
plt.subplot(122)
plt.title('If $l_1 = 1$')
plt.plot(h2_vec0, l2_vec1, color='red')
plt.grid()
plt.ylabel('Labor supply')
plt.xlabel('Human capital in period 2')
plt.xticks([0,0.2,0.4,0.6,0.8,1,1.2,1.4,1.6])
plt.tight_layout()
```

We see that if $l_1=0$ she will get more utility by not working up to a certain point between 0.4 and 0.6 human capital. We will now find exactly this point by optimizing.
So naturally we want to import optimize from scipy.

```python
from scipy import optimize
```

We create a lambda function where we subtract 1 from the the utility she gets from working in period 1, as this is her utility from not working in period 1. So basicly if you look at the first plot which shows the utility, we subtract the green line from the blue line. This way we can find the exact point where the lines cross each other.

```python
#We set a '-' in front of the function as we want to use the optimize.minimize function.
obj = lambda h: -utility(w, h, 1)-1
```

We use the optimize.minimize function to find the wanted value. We use the bounded method as the human capital is bounded.

```python
optimize.minimize_scalar(obj, method='bounded', bounds=[0.1, 1.5])
```

         fun: -0.5666657193115782
     message: 'Solution found.'
        nfev: 26
      status: 0
     success: True
           x: 1.4999957369142178

We find that the solution to the problem is h = 0.5667 which means that if she does not work in period 1, she will get more utility by not working in period 2 as long as her human capital is lesser than 0.5667. Though, when she has more than 0.5667 human capital, she will get more utility by working in period 2.
It is also important to notice that if she decides to work in period 1, she will always reach a higher level of utility by working in period 2. This is because of the gain she get i human capital by working in period 1. No matter what it will get higher than 0.5667, and therefore it is best for her to work.
The solution to the worker's problem is then:
$$
l_2^* = \begin{cases}
1 \quad \text{if} \quad l_1 = 1  \\
1 \quad \text{if} \quad l_1 = 0 \quad \wedge \quad 0.5667 \le h  \\
0 \quad \text{if} \quad l_1 = 0 \quad \wedge \quad h \le 0.5667
\end{cases}
$$

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

First we want to find out how many of the values in h_vec that are smaller or equal to 0.5667:

```python
s = np.logical_not(h_vec >= 0.5666657193115782)
import collections
collections.Counter(s)
```

    Counter({True: 33, False: 67})

We find that there are 33 values in her human capital vector that is lesser than 0.5667. With this information we can define the utility function for period 1.

```python
def utility_v1(w, h, l):
    """Args:
    w : wage
    h : human capital
    l : working dummy

    Returns:
    Utility for the worker in period 1"""

    utilities = []
    #If she chooses not to work:
    if l == 0:

        i = 0
        #She doesn't want to work in period 2 if her human capital is lower than 0.5667
        while i < 33:
            utilities.append(utility(w, h[i], l) + beta*utility(w, h2_vec0[i], 0))
            i +=1

        #She will work in period 2 if her human capital is higher than 0.5667
        while 32 < i < 100:
            utilities.append(utility(w, h[i], l) + beta*utility(w, h2_vec0[i], 1))
            i +=1

    #If she chooses to work:
    else:
        i = 0
        while i < 100:
            utilities.append(utility(w, h[i], l)+ beta*utility(w, h2_vec1[i], 1))
            i +=1
    return utilities
```

Below we see her utility as a function of her human capital. We notice that if she has low human capital, it can pay off not to work in period 1, but as her human capital is rising, it becomes better and better for her to work in period 1.

```python
plt.plot(h_vec, utility_v1(w, h_vec, 0), label='$l_1=0$')
plt.plot(h_vec, utility_v1(w, h_vec, 1), label='$l_1=1$')
plt.legend()
plt.xlabel('Human capital period 1'), plt.ylabel('Utility')
plt.grid()
plt.show()
```

We now make a function which will plot the labor as a function of human capital:

```python
def solve_v1(w):
    """Args:
    w : wage

    Returns:
    A plot showing how the labor supply is as a function of human capital in period 1"""
    loop = np.linspace(0,99,100, dtype=int)
    l1_vec = []

    for i in loop:
        if utility_v1(w, h_vec, 0)[i] < utility_v1(w, h_vec, 1)[i]:
            l1_vec.append(1)
        else:
            l1_vec.append(0)
    plt.plot(h_vec, l1_vec)
    plt.xticks([0,0.2,0.4,0.6,0.8,1,1.2,1.4,1.6])
    plt.ylabel('Labor supply'), plt.xlabel('Human capital period 1')
    plt.grid()
    return plt.show()
solve_v1(w)
```

We see as expected that the labor jumps up at the point where the 2 utility curves cut each other. We will now find which value of h that triggers this movement.

First we create a list that contains the values of the utility of working minus the utility of not working.

```python
def difference(w):
    """Args:
    w : wage

    Returns:
    The difference in utility when she works against the utility when she does not work"""
    diff = []
    i = 0
    while i < 100:
        diff.append(utility_v1(w, h_vec, 1)[i]-utility_v1(w, h_vec, 0)[i])
        i +=1
    return diff
```

Now we find out how many of the values in the list that are lesser than 0:

```python
#We make a list of zeroes to compare with
zeroes = np.zeros(100)
#Make the comparison
count = np.logical_not(difference(w) > zeroes)
#Count the numbers that a lesser than 0
collections.Counter(count)
```

    Counter({True: 18, False: 82})

We see that there are 18 values which are lesser than 0, and 82 values which are greater than 0.
From this we can get the value of human capital where the labor supply makes the jump:

```python
h_vec[17]
```

    0.3404040404040404

Thus the worker maximizes her utility with the following:
$$
l_1^* = \begin{cases}
0 \quad \text{if} \quad h_1 \le 0.34  \\
1 \quad \text{if} \quad 0.34 < h_1
\end{cases}
$$

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

As an example we set the wage to be 0.9 - right under the unemployment benefit.
We plot it in a graph below, where we see that the worker actually can achieve a higher utility by working. We notice that there are 2 ways of getting a higher utility - if she has high human capital she can simply just choose to work in period 2, if she does not have high human capital she needs to work in both periods to achieve a higher utility.

```python
plt.figure(figsize=(8,6))
plt.plot(h2_vec0, utility(0.9, h2_vec0, 1), label = 'If $l_2=1$ and $l_1=0$')
plt.plot(h2_vec0, utility(0.9, h2_vec1, 1), label = 'If $l_2=1$ and $l_1=1$')
plt.plot(h2_vec0, utility_l0            , label = 'If $l_2=0$')
plt.ylabel('Utility ($v_2(h_2)$)'), plt.xlabel('Human Capital in period 2')
plt.legend()
plt.grid()
plt.show()
```

We can also see on the equation for $v_2$ that as soon as the human capital is high enough to fullfill $b = wh_tl_t-\gamma l_t \Leftrightarrow h_t = \frac{b+\gamma l_t}{wl_t}$. As $b>w$ now, this means that $h_t$ needs to be greater than 1.
This makes sense as $wh_tl_t$ only can be greater than $b$ if $h_t$ is greater than 1, and when she decides to work you also have to account for the disutility of working.
Her level of human capital decides how much impact the wage have. Therefore it can be concluded that if the human capital is high enough, the worker will always choose to work, even though the wage is lower than the unemployment benefit.

# 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$, $\phi$, $\gamma$) and $y_{t-1}$ , $\pi_{t-1}$, $v_t$, $s_t$, and $s_{t-1}$.

First, we of course import sympy, and make it print nice results:

```python
import sympy as sm
sm.init_printing(use_unicode=True)
```

Then we define all the parameters and variables as symbols:

```python
y = sm.symbols("y_t")
y1 = sm.symbols("y_t-1")
pi = sm.symbols("pi_t")
pi1 = sm.symbols("pi_t-1")
r = sm.symbols("r_t")
v = sm.symbols("v_t")
i = sm.symbols("i_t")
s = sm.symbols("s_t")
s1 = sm.symbols("s_t-1")
pie = sm.symbols("pi_t^e")
pie1 = sm.symbols("pi_t-1^e")
alpha = sm.symbols("alpha")
h = sm.symbols("h")
b = sm.symbols("b")
gamma = sm.symbols("gamma")
phi = sm.symbols("phi")
```

Now we define both the AD and the SRAS curve.

```python
AD = sm.Eq(pi, (1 / (h*alpha)) * (v - (1 + b*alpha)*y))
SRAS = sm.Eq(pi, pi1 + gamma*y - phi*gamma*y1 + s - phi*s1)
```

```python
def solve_pi(alpha = alpha, h = h, b = b, phi = phi, gamma = gamma, y1 = y1, pi1 = pi1, v = v, s = s, s1 = s1):
    """Args:
    alpha (optional) : alpha value
    h     (optional) : h value
    b     (optional) : b value
    phi   (optional) : phi value
    gamma (optional) : gamma value
    y1    (optional) : outputgap in period t-1
    pi1   (optional) : inflationgap in period t-1
    v     (optional) : demand disturbance
    s     (optional) : supply disturbance
    s1    (optional) : supply disturbance in period t-1

    Returns:
    Equilibrium value for pi"""
    #1. isolate pi in a way that lets us use its value without pi
    ad = sm.solve(AD, pi)[0]

    #2. Isolate y_t in the SRAS equation so we can substitute it in the AD equation
    sub = sm.solve(SRAS, y)[0]

    #3. We substitute it in the AD equation
    subit = ad.subs(y, sub)

    #4. Set up the equation with pi again
    finalAD = sm.Eq(pi, subit)

    #5. Solve for the solution by isolating pi
    solution = sm.solve(finalAD, pi)[0]

    return solution
pi_solve = solve_pi()
```

```python
def solve_y(alpha = alpha, h = h, b = b, phi = phi, gamma = gamma, y1 = y1, pi1 = pi1, v = v, s = s, s1 = s1):
    """Args:
    alpha (optional) : alpha value
    h     (optional) : h value
    b     (optional) : b value
    phi   (optional) : phi value
    gamma (optional) : gamma value
    y1    (optional) : outputgap in period t-1
    pi1   (optional) : inflationgap in period t-1
    v     (optional) : demand disturbance
    s     (optional) : supply disturbance
    s1    (optional) : supply disturbance in period t-1

    Returns:
    Equilibrium value for y"""
    #1. Isolate y and pi so we can use sympy magic on them
    yisolated = sm.solve(AD, y)[0]
    piiso = sm.solve(SRAS, pi)[0]

    #2. Substitute pi into the transformed AD equation
    sub = yisolated.subs(pi, piiso)

    #3. Set up the equation with y again
    finaly = sm.Eq(y, sub)

    #4. Solve for the solution by isoltating y
    solution = sm.solve(finaly, y)[0]

    return solution
y_solve = solve_y()
```

We get the 2 following equilibrium values:

```python
sm.Eq(pi, solve_pi())
```

```python
sm.Eq(y, solve_y())
```

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

First we find the equilibrium value with the given values.
To do this we lambdify our solutions from before, to be able to put numbers in on the letters' place.

```python
func_pi = sm.lambdify((alpha, h, b, phi, gamma, y1, pi1, v, s, s1), pi_solve)
func_y = sm.lambdify((alpha, h, b, phi, gamma, y1, pi1, v, s, s1), y_solve)
```

From this we can now calculate the values:

```python
print(f'Equilibrium value for {pi} is: {func_pi(alpha,h,b,phi,gamma,0,0,0,0,0)} \nEquilibrium value for {y} is: {func_y(alpha,h,b,phi,gamma,0,0,0,0,0)}')
```

    Equilibrium value for pi_t is: 0
    Equilibrium value for y_t is: 0

This makes sense as in the long run we would not expect any output- or inflationgap.

To illustrate the equilibrium, we define the following 2 functions:

```python
def AD_curve(v, T=10):
    """Args:
    v            : demand disturbance
    T (optional) : periods

    Returns:
    Values for the AD-curve in a list"""

    #1. Use the given values
    alpha = par['alpha']
    h = par['h']
    b = par['b']
    phi = par['phi']
    gamma = par['gamma']

    #2. Make an empty list for the values
    AD_values = []

    #Make an 'i' so we get an equal number of negative and positive numbers
    i=-T/2
    while i < T/2:
        AD_values.append(1/(h*alpha)*(v-(1+b*alpha)*i))
        i+=1
    return AD_values
```

```python
def SRAS_curve(pi1, s, s1, T=10):
    """Args:
    pi1          : inflationgap in period t-1
    s            : supply disturbance
    s1           : supply distrubance in period t-1
    T (optional) : periods

    Returns:
    Values for the SRAS-curve in a list"""

    #1. Use the given values
    alpha = par['alpha']
    h = par['h']
    b = par['b']
    phi = par['phi']
    gamma = par['gamma']

    #2. Make an empty list for the values
    SRAS_values = []

    #Make an 'i' so we get an equal number of negative and positive numbers
    i=-T/2
    while i < T/2:
        SRAS_values.append(pi1 +gamma*i -phi*gamma*y1 +s -phi*s1)
        i+=1
    return SRAS_values
```

We now plot both scenarios in 1 plot:

```python
xline = np.linspace(-5,4,10)
plt.plot(xline, AD_curve(0), label = '$AD_0$')
plt.plot(xline, SRAS_curve(0,0,0), label = '$SRAS_0$')
plt.plot(xline, AD_curve(0.1), label = '$AD_{v_t=0.1}$')
plt.legend()
plt.xlim(-0.1,0.1)
plt.ylim(-0.1,0.1)
plt.grid()
plt.xlabel('$y_t$')
plt.ylabel('$\pi_t$')
plt.show()
```

We see clearly that if all the variables are equal to 0, the equilibrium will be in (0,0) as we calculated.
Besides that we see that if the economy experience a positive demand disturbance, the AD-curve shifts up a bit along the SRAS-curve, creating a new short run equilibrium in a point where there is a positive output- and inflationgap.

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

We start by making the movement that both the AD- and the SRAS-curve make. It is important to notice that the SRAS-curve depends on the inflation level from the previous period. Therefore we also simulate how the inflation is changing in order to make the right SRAS-movement.

```python
def evolution(x, T=10):
    """Args:
    x            : value for v in the first period
    T (optional) : periods

    Returns:
    AD- and SRAS-curve values as well as the level of inflation, all as lists"""

    #1. A list that will contain all the values for v during the simulation
    v_values = [x]
    for i in range(0,T):
        v_values.append(par['delta']*v_values[-1])

    #2. A list with values for the AD-curve
    AD_movement = []
    for i in v_values:
        #We use our function from before to easily get the AD-values
        AD_movement.append(AD_curve(i))

    #3. Lists with the inflation- and SRAS-movement
    inflation = [0]
    SRAS_movement = []
    for i in range(0,T):
        #First create the inflation list using the equilibrium function
        inflation.append(func_pi(par['alpha'], par['h'], par['b'], par['phi'], par['gamma'], y1, 0, v_values[i], 0, 0))

    for i in range(0,T):
        #Then create the SRAS-values out of the inflation points
        SRAS_movement.append(SRAS_curve(inflation[i], 0, 0))

    return AD_movement, SRAS_movement, inflation
AD_movement, SRAS_movement, inflation = evolution(0.1)
```

We plot the first 3 periods to inspect:

```python
plt.plot(xline, AD_movement[0],   label = '$AD_0$')
plt.plot(xline, AD_movement[1],   label = '$AD_1$')
plt.plot(xline, AD_movement[2],   label = '$AD_2$')
plt.plot(xline, SRAS_movement[0], label = '$SRAS_0$')
plt.plot(xline, SRAS_movement[1], label = '$SRAS_1$')
plt.plot(xline, SRAS_movement[2], label = '$SRAS_2$')
plt.scatter(0,0)
plt.xlim(-0.04,0.04)
plt.ylim(-0.005,0.005)
plt.grid()
plt.legend(bbox_to_anchor=(1.1,0.8))
plt.xlabel('$y_t$')
plt.ylabel('$\pi_t$')
plt.show()
```

The first period is the same as before where both the output- and inflationgap is rising because of the chock.
In the second period we see that the SRAS-curve jumps up a bit because of the higher inflation in period 1. At the same time, the AD-curve begins to slope downwards on the SRAS-curve, as the chock is gradually becomming lesser. Now the inflationgap is even higher, but the outputgap has become smaller.
In the third period we see that the SRAS-curve is now jumping downwards again. Combined with the AD-curve still shifting downwards, this creates an equilibrium in period 3 where both the output- and inflationgap is lesser than in period 2.
The movement in period 3 will continue to happen until we reach the long run equilibrium where both gaps are 0.

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

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

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

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

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

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

```python
def simulate(x, c ,T, phi=par['phi'], sigmax=par['sigma_x'], sigmac = par['sigma_c']):
    """Args:
    x                 : starting value for v
    c                 : starting value for s
    T                 : simulation periods
    phi (optional)    : value for phi
    sigmax (optional) : standard deviation of x
    sigmac (optional) : standard deviation of c

    Returns:
    A simulation of inflation- and outputgap, as well as values for sigma_x and sigma_c"""

    #1. Set the random seed inside the function so we get the same output every time
    np.random.seed(1)

    #2. Make lists for the future values
    v_values = [x]
    s_values = [c]
    inflation = [0]
    y = [0]

    #3. Append new values
    for i in range(0,T):
        x = np.random.normal(loc=0, scale=sigmax, size=1)
        c = np.random.normal(loc=0, scale=sigmac, size=1)
        v_values.append(par['delta']*v_values[i]+x)
        s_values.append(par['omega']*s_values[i]+c)
        #We use the 2 lambdified functions from earlier
        inflation.append(func_pi(par['alpha'], par['h'], par['b'], phi, par['gamma'], y[-1], inflation[-1], v_values[-1], s_values[i], s_values[i-1]))
        y.append(func_y(par['alpha'], par['h'], par['b'], phi, par['gamma'], y[-1], inflation[-1], v_values[-1], s_values[i], s_values[i-1]))

    return inflation, y, sigmax, sigmac
```

```python
plt.figure(figsize=(17,7))
plt.subplot(211)
plt.xlabel('Period')
plt.ylabel('$\pi_t$')
plt.plot(np.linspace(0,999,1000),simulate(0,0,999)[0], color='blue')
plt.grid()
plt.xlim(0,1000)
plt.subplot(212)
plt.xlabel('Period')
plt.ylabel('$y_t$')
plt.plot(np.linspace(0,999,1000),simulate(0,0,999)[1], color='red')
plt.grid()
plt.xlim(0,1000)
plt.tight_layout()
plt.show()
```

We see as expected that there are more persistence in the inflation.
We now calculate the 5 statistics:

```python
print(f'Variance of y is: {np.var(simulate(0,0,999)[1]):0.5f}\n')
print(f'Variance of \u03C0 is: {np.var(simulate(0,0,999)[0]):0.5f}\n')
print(f'The correlation between y and \u03C0 is: {np.corrcoef(simulate(0,0,999)[1],simulate(0,0,999)[0])[0,1]:0.5f}\n')
print(f'The auto-correlation for y is: {np.corrcoef(simulate(0,0,999)[1][1:],simulate(0,0,999)[1][:-1],rowvar=False)[0,1]:0.5f}\n')
print(f'The auto-correlation for \u03C0 is: {np.corrcoef(simulate(0,0,999)[0][1:],simulate(0,0,999)[0][:-1],rowvar=False)[0,1]:0.5f}\n')
```

    Variance of y is: 1.95761

    Variance of π is: 1.05659

    The correlation between y and π is: -0.14297

    The auto-correlation for y is: 0.78563

    The auto-correlation for π is: 0.97741

We again see as expected, that the auto-correlation for $\pi$ is higher than for $y$.

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

First we make a function which will give us a list containing the correlation between $y_t$ and $\pi_t$ for 101 different values between 0 and 1.

```python
def phichange():
    """Args:
    None
    Returns:
    Correlation values for y and pi as a list"""

    #1. Make an empty list for values
    correlation = []
    #2. Fill in values of correlation
    for i in np.linspace(0,1,101):
        correlation.append(np.corrcoef(simulate(0,0,999,i)[1], simulate(0,0,999,i)[0])[0,1])
    return correlation
```

We will now find which value of $\phi$ that gives a correlation that is close to 0.31.
We start by making a function which gives us the correlation in our simulation that is closest to 0.31:

```python
def close_to(list,value):
    """Args:
    list  : the list you want to examine
    value : the value you want to be close to

    Returns:
    The value in the list that comes closest to the given value"""

    #1. Make the list to a numpy array for the use of numpy-functions
    array = np.asarray(list)

    #2. Find out which index gives the closest value
    index = np.abs((array-value)).argmin()
    return array[index]
```

We check if it is working, and see that the closet value in our simulation is actually really close to being 0.31 straight.

```python
close_to(phichange(),0.31)
```

Now we use the brentq-method, which is considered as the best of the rootfinding routines, to find out which value of $\phi$ is the best. (It takes a little time to run it because of all the simulations)

```python
optimize.brentq(lambda i: np.asarray(phichange())[int(i)] - close_to(phichange(),0.31),0,100)
```

We get the value 95.405 which is the index for the value of $\phi$ where the correlation between $y_t$ and $\pi_t$ is closest to 0.31, in our simulation. We round it down to 95 so we can use it as an index in our 'phichange()'-function, to check if it gives us the right value:

```python
phichange()[95]
```

We see that it is the value that is closest to 0,31, and can thereby confirm that 0.95405 must be the $\phi$ that gives us the wanted correlation, as our indexing is just the index divided by 100.
Below, you can see how the correlation changes with different values of $\phi$. We marked out the point where the correlation is close to 0.31.

```python
plt.plot(np.linspace(0,1,101),phichange(), color='blue')
plt.xlabel('$\phi$')
plt.ylabel('$corr(y_t,\pi_t)$')
plt.axhline(0.31, linestyle = 'dashed', color = 'red')
plt.axvline(0.9540525818374589, linestyle = 'dashed', color = 'red')
plt.grid()
plt.show()
```

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

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

After numerous attempts, we still haven't got the perfect solution. Below you can see our best considerations to question 6.

First we define a function which returns all the wanted values:

```python
def varcorr(x=0,c=0,T=999,phi=par['phi'],sigmax=par['sigma_x'],sigmac = par['sigma_c']):

    vary       = np.var(simulate(x,c,T,phi,sigmax,sigmac)[1])
    varpi      = np.var(simulate(x,c,T,phi,sigmax,sigmac)[0])
    corrypi    = np.corrcoef(simulate(x,c,T,phi,sigmax,sigmac)[1],simulate(x,c,T,phi,sigmax,sigmac)[0])[0,1]
    autocorry  = np.corrcoef(simulate(x,c,T,phi,sigmax,sigmac)[1][1:],simulate(x,c,T,phi,sigmax,sigmac)[1][:-1],rowvar=False)[0,1]
    autocorrpi = np.corrcoef(simulate(x,c,T,phi,sigmax,sigmac)[0][1:],simulate(x,c,T,phi,sigmax,sigmac)[0][:-1],rowvar=False)[0,1]

    return vary, varpi, corrypi, autocorry, autocorrpi
```

Then we make a lambda function which will make it easier to minimize the difference in the statistics.

```python
mini = lambda a,b,c: np.array(varcorr(phi=a, sigmax=b, sigmac=c))-np.array([1.64, 0.21, 0.31, 0.84, 0.48])
```

We get the following array where we want all the numbers to be as close to 0 as possible, so we are as close to the US business cycle as possible.

```python
mini(0,par['sigma_x'],par['sigma_c'])
```

    array([ 0.31761293,  0.84659291, -0.45297083, -0.05437124,  0.49740805])

From here we wanted to use a numerical optimizer such as optimize.minimize or optimize.root to find the values of $\sigma_x, \sigma_c$ and $\phi$. We are aware that a numerical optimizer such as brentq or newton can't be used in this case, as we with our simulated model can't get the perfect statistics where every value in 'mini' is 0.
We just couldn't crack the code on how to define our lambda function in a different way that would make it possible for us to use the solvers.

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

First we import the necessary packages:

```python
import pandas as pd
from ipywidgets import interact, interactive, fixed, interact_manual
import ipywidgets as widgets
from mpl_toolkits.mplot3d import Axes3D
```

To plot the budget shares, we define each individual budget share $\beta_i$ from betas, and plot these in a figure with all betas. Afterwards we create an interactive figure to choose each betadistribution individually

```python
#First we define beta one, two and three (Index starts with 0)
beta_1 = betas[:,0]
beta_2 = betas[:,1]
beta_3 = betas[:,2]
```

```python
#Secondly we define a DataFrame with each beta
Budget_share = pd.DataFrame([beta_1, beta_2, beta_3])

#And transpose
Budget_share = pd.DataFrame(Budget_share).transpose()

#Rename the coloumns
Budget_share = pd.DataFrame(Budget_share).rename(columns={0:"beta_1",1:"beta_2",2:"beta_3"})
```

```python
#Plotting the beta's against each other
Budget_share.plot.hist(bins=300)
```

```python
#Define a functions that creates the figure for each beta.
def budget_share(beta_value):
    pd.DataFrame(Budget_share).hist(column=beta_value, bins=100)
    plt.xlabel('Budget share value')
    plt.ylabel('Number of persons')
    plt.title(f'Budget share across agents for {str(beta_value)}')
    plt.grid(True)
    return plt.show()
```

```python
#Create an interactive figure that shows the distribution for each beta (Beware of the changeing axis)
widgets.interact(budget_share, beta_value = widgets.Dropdown(options=["beta_1","beta_2","beta_3"]))
```

    interactive(children=(Dropdown(description='beta_value', options=('beta_1', 'beta_2', 'beta_3'), value='beta_1…

    <function __main__.budget_share(beta_value)>

#### Comments for the histograms:

It appears that the agents have a preference for good one, as beta_1 has the highest expected value, even though it also has a large spread. Good three is generally not preferred with a very low expected value and low spread. Finally good two has a low expected value, but also a relatively large spread.

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.

To plot the excess demand functions, we first define the demand functions for each good, and thereafter create a function for each individual excess demand. Finally these are plotted. (The DataFrame method was not as suitable for this task, so we use numpy grids)

```python
#First we define the demand functions
def demand_func1(beta_1, p1, p2, e1, e2, e3):
    I = p1*e1+p2*e2+e3
    return betas[:,0]*I/p1

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

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

```python
#Secondly, we can define the excess demand functions
def excess_demand1(betas, p1, p2, e1, e2, e3):
    demand1 = np.sum(demand_func1(betas, p1, p2, e1, e2, e3))
    supply1 = np.sum(e1)
    excess_demand = demand1-supply1
    return excess_demand

def excess_demand2(betas, p1, p2, e1, e2, e3):
    demand2 = np.sum(demand_func2(betas, p1, p2, e1, e2, e3))
    supply2 = np.sum(e2)
    excess_demand  = demand2-supply2
    return excess_demand

def excess_demand3(betas, p1, p2, e1, e2, e3):
    demand3 = np.sum(demand_func3(betas, p1, p2, e1, e2, e3))
    supply3 = np.sum(e3)
    excess_demand = demand3-supply3
    return excess_demand
```

```python
#Thirdly, we define the prices. First by defineing the numpy arrays for each price, thereafter create empty grids and insert them by mesh and enumerate.
size = 100
p1_gen = np.linspace(1, 10, size)
p2_gen = np.linspace(1, 10, size)

p1_empty_grid = np.empty((size, size))
p2_empty_grid = np.empty((size, size))

p1_grid, p2_grid = np.meshgrid(p1_gen, p2_gen)
for i, p1 in enumerate(p1_gen):
    for j, p2 in enumerate(p2_gen):
        p1_empty_grid[i,j] = excess_demand1(betas, p1, p2, e1, e2, e3)
        p2_empty_grid[i,j] = excess_demand2(betas, p1, p2, e1, e2, e3)
```

```python
#Now that we have the data, we just need to plot it
#Plotting for excess good 1
fig = plt.figure(dpi=100, figsize=(12,5))
axis1 = fig.add_subplot(121, projection = '3d')
fig1 = axis1.plot_surface(p1_grid, p2_grid, p1_empty_grid, cmap=plt.cm.inferno)
axis1.set_title('Excess demand for good 1')
axis1.set_xlabel("p1")
axis1.set_ylabel("p2")
axis1.invert_yaxis()

#And the excess for good 2
axis2 = fig.add_subplot(122, projection = '3d')
fig2 = axis2.plot_surface(p1_grid, p2_grid, p2_empty_grid, cmap=plt.cm.inferno)
axis2.set_title('Excess demand for good 2')
axis2.set_xlabel("p1")
axis2.set_ylabel("p2")
axis2.invert_yaxis()
```

#### Conclusions for excess demand plots:
First of all, its important to beware of the change in axis labels on the z axis. Furthermore, the opvious difference between the figures is the direction of the plot. Where the excess demand of good 1 increases with an increase in p1 and a decrease in p2, the excess demand of good 2 increases when p1 decreases and p2 increases. This makes sense, as the excess demand follows the goods own price for both goods.

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

The solution for this question, and the following two, relies heavily on the code from problemset 2. We assume the Walras-equilibrium is found where demand for good three is zero.

```python
#As most parameters and the demand and excess demand functions are already set up, we start by defineing the equilibirum function as in problemset 2
def find_equilibrium(alphas, p1, p2, e1, e2, e3, kappa=0.5, eps=1e-8, maxiter=500):

    t = 0
    while True:
        # a. step 1: define excess demand
        Z1 = excess_demand1(alphas, p1, p2, e1, e2, e3)
        Z2 = excess_demand2(alphas, p1, p2, e1, e2, e3)

        # b: step 2: Define when to stop?
        if  np.abs(Z1) < eps and np.abs(Z2) < eps and t >= maxiter:
            print(f'{t:3d}: p1 = {p1:12.8f} -> excess demand -> {Z1:14.8f} and p2 = {p2:18.8f} -> excess demand {Z2:14.8f}')
            break
        # c. step 3: update p1 and p2
        p1 = p1 + kappa*Z1/alphas.size
        p2 = p2 + kappa*Z2/alphas.size

        # d. step 4: make function return values for both p1 and p2 with modified iteration printout
        if t < 5 or t%500 == 0:
            print(f'{t:3d}: p1 = {p1:12.8f} -> excess demand -> {Z1:14.8f} and p2 = {p2:18.8f} -> excess demand {Z2:14.8f}')
        elif t == 5:
            print('   ...')
        t += 1

# e. call find equilibrium function with defined parameters
p1 = 1
p2 = 1
kappa = 0.5
eps = 1e-8
p1 = find_equilibrium(alphas, p1, p2, e1, e2, e3, kappa=kappa, eps=eps)

```

      0: p1 =   1.15434354 -> excess demand -> 46303.06210182 and p2 =         0.96327349 -> excess demand -11017.95327569
      1: p1 =   1.27671409 -> excess demand -> 36711.16402205 and p2 =         0.93670601 -> excess demand -7970.24375230
      2: p1 =   1.37944193 -> excess demand -> 30818.35277116 and p2 =         0.91847489 -> excess demand -5469.33650090
      3: p1 =   1.46868506 -> excess demand -> 26772.93962492 and p2 =         0.90711916 -> excess demand -3406.71837664
      4: p1 =   1.54806962 -> excess demand -> 23815.36779893 and p2 =         0.90137981 -> excess demand -1721.80611963
       ...
    500: p1 =   5.93547693 -> excess demand ->   555.09286573 and p2 =         2.40971084 -> excess demand   207.27799461
    1000: p1 =   6.37712322 -> excess demand ->   105.23505785 and p2 =         2.57456027 -> excess demand    39.26553955
    1500: p1 =   6.46578148 -> excess demand ->    22.31026660 and p2 =         2.60763836 -> excess demand     8.32331914
    2000: p1 =   6.48478182 -> excess demand ->     4.83263398 and p2 =         2.61472674 -> excess demand     1.80286534
    2500: p1 =   6.48890693 -> excess demand ->     1.05158964 and p2 =         2.61626565 -> excess demand     0.39230425
    3000: p1 =   6.48980501 -> excess demand ->     0.22905431 and p2 =         2.61660069 -> excess demand     0.08545050
    3500: p1 =   6.49000065 -> excess demand ->     0.04990272 and p2 =         2.61667367 -> excess demand     0.01861659
    4000: p1 =   6.49004327 -> excess demand ->     0.01087252 and p2 =         2.61668957 -> excess demand     0.00405608
    4500: p1 =   6.49005256 -> excess demand ->     0.00236887 and p2 =         2.61669304 -> excess demand     0.00088372
    5000: p1 =   6.49005458 -> excess demand ->     0.00051612 and p2 =         2.61669379 -> excess demand     0.00019254
    5500: p1 =   6.49005502 -> excess demand ->     0.00011245 and p2 =         2.61669395 -> excess demand     0.00004195
    6000: p1 =   6.49005512 -> excess demand ->     0.00002450 and p2 =         2.61669399 -> excess demand     0.00000914
    6500: p1 =   6.49005514 -> excess demand ->     0.00000534 and p2 =         2.61669400 -> excess demand     0.00000199
    7000: p1 =   6.49005514 -> excess demand ->     0.00000116 and p2 =         2.61669400 -> excess demand     0.00000043
    7500: p1 =   6.49005515 -> excess demand ->     0.00000025 and p2 =         2.61669400 -> excess demand     0.00000009
    8000: p1 =   6.49005515 -> excess demand ->     0.00000006 and p2 =         2.61669400 -> excess demand     0.00000002
    8500: p1 =   6.49005515 -> excess demand ->     0.00000001 and p2 =         2.61669400 -> excess demand     0.00000000
    8561: p1 =   6.49005515 -> excess demand ->     0.00000001 and p2 =         2.61669400 -> excess demand     0.00000000

#### Conclusions for equilibrium prices:
With the demand for good 3 being zero, we get a Walras-equilibrium where **p1 = approx. 6.49 and p2 = approx 2.62**

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

To plot the distribution of utility in the Walras-equilibrium, we generate the function for utility as seen in the start of the task, and use the prices found in Q3 to find the different distributions. <br/>

We do this by defining the demand functions for each good, multiply with the beta values for each good, and return it all through the generated utility function. Finally we plot the results with the DataFrame method as Q1. We thereafter use numpy to find mean, median, variance, and max.

```python
#First generate the demandfunctions and the beta functions, afterwards we return them through the utility function
def Walras_equilibrium_utilityfunction(betas, p1, p2, e1, e2, e3, gamma):
    Demand1 = demand_func1(beta_1, p1, p2, e1, e2, e3)
    Demand2 = demand_func2(beta_1, p1, p2, e1, e2, e3)
    Demand3 = demand_func3(beta_1, p1, p2, e1, e2, e3)

    Beta1 = np.array([x[0] for x in betas])
    Beta2 = np.array([x[1] for x in betas])
    Beta3 = np.array([x[2] for x in betas])

    return ((Demand1*Beta1)*(Demand2*Beta2)*(Demand3*Beta3))**gamma
```

```python
#Secondly we define our prices from Q3, run them through our Walras-Utilityfunction and plot the results using pandas.
p1 = 6.490
p2 = 2.617
Walras_data_func = Walras_equilibrium_utilityfunction(betas,p1,p2,e1,e2,e3,gamma)
Walras_data = pd.DataFrame(Walras_data_func).rename(columns={0:'Nr. of Agents'})

pd.DataFrame(Walras_data).plot(kind="hist",title="Distribution of Walras utility",grid=True,bins=10, cmap=plt.cm.summer)
plt.axis([0,6,0,50000])

pd.DataFrame(Walras_data).plot(kind="hist",title="Distribution of Walras utility zoom 1",grid=True,bins=40, cmap=plt.cm.autumn)
plt.axis([0,1,0,50000])

pd.DataFrame(Walras_data).plot(kind="hist",title="Distribution of Walras utility zoom 2",grid=True,bins=200, cmap=plt.cm.winter)
plt.axis([0,0.15,0,50000])

pd.DataFrame(Walras_data).plot(kind="box",title="Boxplot of the distribution of Walras utility", grid=True, cmap=plt.cm.spring)
plt.axis([0,2,0,6])
```

```python
#Finally we simply calculate mean and variance, and max, using numpy
print("The mean is ", np.mean(Walras_data_func))
print("The median is", np.median(Walras_data_func))
print("The variance is", np.var(Walras_data_func))
print("The highest observation is ", np.max(Walras_data_func))
```

    The mean is  0.06535541853385282
    The median is 0.014873998664104271
    The variance is 0.03134762216019003
    The highest observation is  5.897938370865469

#### Conclusion on Q4:
We find that the vast majority of the Walras utilities gather in the low end of the distribution with a rather low variance. The highest observed utility is around 5.89, which is much higher than the mean. As the median is lower than the mean, this indicates right skewedness, which is also clear from the boxplot, or lack of one, due to frequent outliers in the high end og the values.

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

To find the Walras-equilibrium prices for equally distributed endownments, the endownments are redefined as the mean of their individual values, and put through the same process as Q2, Q3 and Q4. Finally, we create a interactive figure to show the effects of changes in gamma on the distribution of utility.

```python
#Redefine each endownment using numpy taking the mean and using np.zeros so an array is generated insted of a single value
e1 = np.mean(e1)+np.zeros(N)
e2 = np.mean(e2)+np.zeros(N)
e3 = np.mean(e3)+np.zeros(N)
```

```python
#Same procedure as Q2,Q3 and Q4:
#First we define the demand functions
def demand_func1(beta_1, p1, p2, e1, e2, e3):
    I = p1*e1+p2*e2+e3
    return betas[:,0]*I/p1

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

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

```python
#Secondly, we can define the excess demand functions
def excess_demand1(betas, p1, p2, e1, e2, e3):
    demand1 = np.sum(demand_func1(betas,p1,p2,e1,e2,e3))
    supply1 = np.sum(e1)
    excess_demand = demand1-supply1
    return excess_demand

def excess_demand2(betas, p1, p2, e1, e2, e3):
    demand2 = np.sum(demand_func2(betas,p1,p2,e1,e2,e3))
    supply2 = np.sum(e2)
    excess_demand  = demand2-supply2
    return excess_demand

def excess_demand3(betas, p1, p2, e1, e2, e3):
    demand3 = np.sum(demand_func3(betas,p1,p2,e1,e2,e3))
    supply3 = np.sum(e3)
    excess_demand = demand3-supply3
    return excess_demand
```

```python
#As most parameters and the demand and excess demand functions are already set up, we start by defining the equilibirum function
def find_equilibrium(alphas, p1, p2, e1, e2, e3, kappa=0.5, eps=1e-8, maxiter=500):

    t = 0
    while True:
        # a. step 1: define excess demand
        Z1 = excess_demand1(alphas,p1,p2,e1,e2,e3)
        Z2 = excess_demand2(alphas,p1,p2,e1,e2,e3)

        # b: step 2: Define when to stop?
        if  np.abs(Z1) < eps and np.abs(Z2) < eps and t >= maxiter:
            print(f'{t:3d}: p1 = {p1:12.8f} -> excess demand -> {Z1:14.8f} and p2 = {p2:18.8f} -> excess demand {Z2:14.8f}')
            break
        # c. step 3: update p1 and p2
        p1 = p1 + kappa*Z1/alphas.size
        p2 = p2 + kappa*Z2/alphas.size

        # d. step 4: make function return values for both p1 and p2 with modified iteration printout
        if t < 5 or t%500 == 0:
            print(f'{t:3d}: p1 = {p1:12.8f} -> excess demand -> {Z1:14.8f} and p2 = {p2:18.8f} -> excess demand {Z2:14.8f}')
        elif t == 5:
            print('   ...')
        t += 1

# e. call find equilibrium function with defined parameters
p1 = 1
p2 = 1
kappa = 0.5
eps = 1e-8
p1 = find_equilibrium(alphas,p1,p2,e1,e2,e3,kappa=kappa,eps=eps)

```

      0: p1 =   1.15443484 -> excess demand -> 46330.45322633 and p2 =         0.96321076 -> excess demand -11036.77294234
      1: p1 =   1.27685472 -> excess demand -> 36725.96313815 and p2 =         0.93659869 -> excess demand -7983.62083163
      2: p1 =   1.37961171 -> excess demand -> 30827.09694193 and p2 =         0.91833778 -> excess demand -5478.27168623
      3: p1 =   1.46887242 -> excess demand -> 26778.21126920 and p2 =         0.90696448 -> excess demand -3411.99200640
      4: p1 =   1.54826733 -> excess demand -> 23818.47424070 and p2 =         0.90121737 -> excess demand -1724.13206651
       ...
    500: p1 =   5.93315437 -> excess demand ->   554.03181223 and p2 =         2.41062773 -> excess demand   207.16645183
    1000: p1 =   6.37364924 -> excess demand ->   104.82980595 and p2 =         2.57527414 -> excess demand    39.16838681
    1500: p1 =   6.46189659 -> excess demand ->    22.17738553 and p2 =         2.60824444 -> excess demand     8.28518999
    2000: p1 =   6.48076821 -> excess demand ->     4.79334756 and p2 =         2.61529454 -> excess demand     1.79068332
    2500: p1 =   6.48485636 -> excess demand ->     1.04073422 and p2 =         2.61682178 -> excess demand     0.38879177
    3000: p1 =   6.48574442 -> excess demand ->     0.22618678 and p2 =         2.61715353 -> excess demand     0.08449750
    3500: p1 =   6.48593744 -> excess demand ->     0.04916853 and p2 =         2.61722564 -> excess demand     0.01836808
    4000: p1 =   6.48597940 -> excess demand ->     0.01068876 and p2 =         2.61724132 -> excess demand     0.00399304
    4500: p1 =   6.48598853 -> excess demand ->     0.00232366 and p2 =         2.61724472 -> excess demand     0.00086806
    5000: p1 =   6.48599051 -> excess demand ->     0.00050515 and p2 =         2.61724547 -> excess demand     0.00018871
    5500: p1 =   6.48599094 -> excess demand ->     0.00010982 and p2 =         2.61724563 -> excess demand     0.00004102
    6000: p1 =   6.48599103 -> excess demand ->     0.00002387 and p2 =         2.61724566 -> excess demand     0.00000892
    6500: p1 =   6.48599105 -> excess demand ->     0.00000519 and p2 =         2.61724567 -> excess demand     0.00000194
    7000: p1 =   6.48599106 -> excess demand ->     0.00000113 and p2 =         2.61724567 -> excess demand     0.00000042
    7500: p1 =   6.48599106 -> excess demand ->     0.00000025 and p2 =         2.61724567 -> excess demand     0.00000009
    8000: p1 =   6.48599106 -> excess demand ->     0.00000005 and p2 =         2.61724567 -> excess demand     0.00000002
    8500: p1 =   6.48599106 -> excess demand ->     0.00000001 and p2 =         2.61724567 -> excess demand     0.00000000
    8549: p1 =   6.48599106 -> excess demand ->     0.00000001 and p2 =         2.61724567 -> excess demand     0.00000000

The new prices are **p1 = 6.486 and p2 = 2.617**

```python
#Now generate the new demandfunctions and the beta functions
def Walras_equalendownment_equilibrium_utilityfunction(betas, p1, p2, e1, e2, e3, gamma):
    Demand1 = demand_func1(betas,p1,p2,e1,e2,e3)
    Demand2 = demand_func2(betas,p1,p2,e1,e2,e3)
    Demand3 = demand_func3(betas,p1,p2,e1,e2,e3)

    Beta1 = np.array([x[0] for x in betas])
    Beta2 = np.array([x[1] for x in betas])
    Beta3 = np.array([x[2] for x in betas])

    return ((Demand1*Beta1)*(Demand2*Beta2)*(Demand3*Beta3))**gamma
```

```python
#Finally we define our prices from Q3, run them through our Walras-Utilityfunction and plot the results using pandas.
p1 = 6.486
p2 = 2.617
Walras_data_func = Walras_equalendownment_equilibrium_utilityfunction(betas,p1,p2,e1,e2,e3,gamma)
Walras_data = pd.DataFrame(Walras_data_func).rename(columns={0:'Nr. of Agents'})

pd.DataFrame(Walras_data).plot(kind="hist",title="Distribution of Walras utility",grid=True,bins=200, cmap=plt.cm.summer)
plt.axis([0,0.14,0,800])

pd.DataFrame(Walras_data).plot(kind="hist",title="Distribution of Walras utility zoom 1",grid=True,bins=200, cmap=plt.cm.autumn)
plt.axis([0,0.06,0,800])

pd.DataFrame(Walras_data).plot(kind="box",title="Boxplot of the distribution of Walras utility", cmap=plt.cm.spring)
plt.axis([0,2,0,0.15])
```

```python
#Again we simply calculate mean, median, variance, and max, using numpy
print("The mean is ", np.mean(Walras_data_func))
print("The median is", np.median(Walras_data_func))
print("The varianse is", np.var(Walras_data_func))
print("The highest observation is ", np.max(Walras_data_func))
```

    The mean is  0.03440934151059249
    The median is 0.026899972248576272
    The varianse is 0.000759513584718288
    The highest observation is  0.1363094967261889

```python
#And to add a little extra interpretation of the value of gamma, create a interactive figure to see the difference
def Walras_equalendow_slider_func(betas, p1, p2, e1, e2, e3, gamma):

    Demand1 = demand_func1(betas,p1,p2,e1,e2,e3)
    Demand2 = demand_func2(betas,p1,p2,e1,e2,e3)
    Demand3 = demand_func3(betas,p1,p2,e1,e2,e3)

    Beta1 = np.array([x[0] for x in betas])
    Beta2 = np.array([x[1] for x in betas])
    Beta3 = np.array([x[2] for x in betas])

    final = ((Demand1*Beta1)*(Demand2*Beta2)*(Demand3*Beta3))**gamma
    final = pd.DataFrame(final).rename(columns={0:'Nr. of Agents'})
    pd.DataFrame(final).plot(kind="hist",title="Distribution of Walras utility given different gammas",grid=True, cmap=plt.cm.ocean)

    return plt.show()

widgets.interact(Walras_equalendow_slider_func,
                 betas=fixed(betas),
                 p1 = fixed(p1),
                 p2 = fixed(p2),
                 e1 = fixed(e1),
                 e2 = fixed(e2),
                 e3 = fixed(e3),
                 gamma=(0,1.5,0.1))
```

    interactive(children=(FloatSlider(value=0.7000000000000001, description='gamma', max=1.5), Output()), _dom_cla…

    <function __main__.Walras_equalendow_slider_func(betas, p1, p2, e1, e2, e3, gamma)>

#### Conclusions for Q5:

As can we seen from both the new figures, and calculations, the mean and median are both lower, but the right skewedness is still present. There are much fewer outliers, and the ones that excist are closer to the other values. In other words, the variance is much lower, and the highest observation is also much lower. As can be interpreted from the interactive figure, gamma in fact has an effect on both mean and variance. Low gamma values, has the highest means, with 0 having a mean of one. High gamma values decrease the mean, which can be seen when moving the slider.

```python

```


```python
from IPython.core.display import HTML
HTML("<style>.container { width:95% !important; }</style>")
```

# Lecture 12, Methods for multiobjective optimization-part 2

##  Our example problem for this lecture

We study a hypothetical decision problem of buying a car, when you can choose to have a car with power between (denoted by $p$) 50 and 200 kW and average consumption (denoted by $c$) per 100 km between 3 and 10 l. However, in addition to the average consumption and power, you need to decide the volume of the cylinders (v), which may be between 1000 $cm^3$ and 4000 $cm^3$. Finally, the price of the car follows now a function

$$
\left(\sqrt{\frac{p-50}{50}}\\
+\left(\frac{p-50}{50}\right)^2+0.3(10-c)\\ +10^{-5}\left(v-\left(1000+3000\frac{p-50}{150}\right)\right)^2\right)10000\\+5000
$$

in euros. This problem can be formulated as a multiobjective optimization problem

$$
\begin{align}
\min \quad & \{c,-p,P\},\\
\text{s.t. }\quad
&50\leq p\leq 200\\
&3\leq c\leq 10\\
&1000\leq v\leq 4000,\\
\text{where }\quad&P = \left(\sqrt{\frac{p-50}{50}}+\left(\frac{p-50}{50}\right)^2+0.3(10-c)\right.\\
& \left.+ 10^{-5}\left(v-\left(1000+3000\frac{p-50}{150}\right)\right)^2\right)10000+5000
\end{align}
$$

```python
#Let us define a Python function which returns the value of this
import math
def car_problem(c,p,v):
#    import pdb; pdb.set_trace()
    return [#Objective function values
        c,-p,
        (math.sqrt((p-50.)/50.)+((p-50.)/50.)**2+
        0.3*(10.-c)+0.00001*(v-(1000.+3000.*(p-50.)/150.))**2)*10000.
        +5000.]
```

### Normalized car problem

```python
#Let us define a Python function which returns the value of this
import math
def car_problem_normalized(c,p,v):
    z_ideal = [3.0, -200.0, 5000]
    z_nadir = [10,-50,1033320.5080756888]
#    import pdb; pdb.set_trace()
    z = car_problem(c,p,v)
    return [(zi-zideali)/(znadiri-zideali) for
            (zi,zideali,znadiri) in zip(z,z_ideal,z_nadir)]
```

```python
z_ideal = [3.0, -200.0, 5000]
z_nadir = [10.,-50,1033320.5080756888]
```

**From now on, we will deal with the normalized problem, although, we write just $f$.** The aim of this is to simplify presentation.

## No preference methods

* Usually only for situations, where the decision maker is not available or does not want to get involved
* These just compute a single Pareto optimal solution, which is in somehow mathematically thought as the best compromise

## Notation

For short, let us denote the feasible set as $S = \{x\in\mathbb R^n: g_j(x) \geq 0 \text{ for all }j=1,\ldots,J \text{ and } h_k(x) = 0\text{ for all }k=1,\ldots,K\}$.

### Method of Global criterion

Involved minimization of the p-norm of $f(x)-z^{ideal}$, that is we solve the problem

$$
\min_{x\in S} \|f(x) - z^{ideal}\|_p
$$

**An optimal solution to this problem is Pareto optimal**

### Applying to our problem studied

```python
import numpy as np
from scipy.optimize import minimize
import ad
def global_criterion_method(f,p):
    #Ideal for any normalized problem is (0,0,0)
    ideal = [0,0,0]
    bounds = ((3,10),(50,200),(1000,4000)) #Variable bounds of the problem
    #Objective is the norm of objective function values minus
    #the ideal
    obj = lambda x: np.linalg.norm(np.array(f(x[0],x[1],x[2]))
                                   -np.array(ideal),ord=p)
    start = [7,125,2500] #starting values for the variables
    res=minimize(
            #Minimize p distance from the ideal
            obj, start, method='SLSQP'
            #Jacobian using automatic differentiation
            ,jac=ad.gh(obj)[0]
            #bounds given above
            ,bounds = bounds,options = {'disp':True})
    return res.x
```

```python
degree = 2
solution = global_criterion_method(car_problem_normalized,
                                           degree)
print("variable values are ",solution)
f_global_criterion = car_problem(solution[0],solution[1],solution[2])
print("objective values are ",f_global_criterion)
```

    Optimization terminated successfully    (Exit mode 0)
                Current function value: 0.2550330331982373
                Iterations: 13
                Function evaluations: 22
                Gradient evaluations: 13
    variable values are  [   3.02944133  176.80589468 2500.00083783]
    objective values are  [3.029441328653617, -176.8058946793659, 213509.66458173335]

```python
help (np.linalg.norm)
```

    Help on function norm in module numpy.linalg:

    norm(x, ord=None, axis=None, keepdims=False)
        Matrix or vector norm.

        This function is able to return one of eight different matrix norms,
        or one of an infinite number of vector norms (described below), depending
        on the value of the ``ord`` parameter.

        Parameters
        ----------
        x : array_like
            Input array.  If `axis` is None, `x` must be 1-D or 2-D, unless `ord`
            is None. If both `axis` and `ord` are None, the 2-norm of
            ``x.ravel`` will be returned.
        ord : {non-zero int, inf, -inf, 'fro', 'nuc'}, optional
            Order of the norm (see table under ``Notes``). inf means numpy's
            `inf` object. The default is None.
        axis : {None, int, 2-tuple of ints}, optional.
            If `axis` is an integer, it specifies the axis of `x` along which to
            compute the vector norms.  If `axis` is a 2-tuple, it specifies the
            axes that hold 2-D matrices, and the matrix norms of these matrices
            are computed.  If `axis` is None then either a vector norm (when `x`
            is 1-D) or a matrix norm (when `x` is 2-D) is returned. The default
            is None.

            .. versionadded:: 1.8.0

        keepdims : bool, optional
            If this is set to True, the axes which are normed over are left in the
            result as dimensions with size one.  With this option the result will
            broadcast correctly against the original `x`.

            .. versionadded:: 1.10.0

        Returns
        -------
        n : float or ndarray
            Norm of the matrix or vector(s).

        See Also
        --------
        scipy.linalg.norm : Similar function in SciPy.

        Notes
        -----
        For values of ``ord < 1``, the result is, strictly speaking, not a
        mathematical 'norm', but it may still be useful for various numerical
        purposes.

        The following norms can be calculated:

        =====  ============================  ==========================
        ord    norm for matrices             norm for vectors
        =====  ============================  ==========================
        None   Frobenius norm                2-norm
        'fro'  Frobenius norm                --
        'nuc'  nuclear norm                  --
        inf    max(sum(abs(x), axis=1))      max(abs(x))
        -inf   min(sum(abs(x), axis=1))      min(abs(x))
        0      --                            sum(x != 0)
        1      max(sum(abs(x), axis=0))      as below
        -1     min(sum(abs(x), axis=0))      as below
        2      2-norm (largest sing. value)  as below
        -2     smallest singular value       as below
        other  --                            sum(abs(x)**ord)**(1./ord)
        =====  ============================  ==========================

        The Frobenius norm is given by [1]_:

            :math:`||A||_F = [\sum_{i,j} abs(a_{i,j})^2]^{1/2}`

        The nuclear norm is the sum of the singular values.

        Both the Frobenius and nuclear norm orders are only defined for
        matrices and raise a ValueError when ``x.ndim != 2``.

        References
        ----------
        .. [1] G. H. Golub and C. F. Van Loan, *Matrix Computations*,
               Baltimore, MD, Johns Hopkins University Press, 1985, pg. 15

        Examples
        --------
        >>> from numpy import linalg as LA
        >>> a = np.arange(9) - 4
        >>> a
        array([-4, -3, -2, ...,  2,  3,  4])
        >>> b = a.reshape((3, 3))
        >>> b
        array([[-4, -3, -2],
               [-1,  0,  1],
               [ 2,  3,  4]])

        >>> LA.norm(a)
        7.745966692414834
        >>> LA.norm(b)
        7.745966692414834
        >>> LA.norm(b, 'fro')
        7.745966692414834
        >>> LA.norm(a, np.inf)
        4.0
        >>> LA.norm(b, np.inf)
        9.0
        >>> LA.norm(a, -np.inf)
        0.0
        >>> LA.norm(b, -np.inf)
        2.0

        >>> LA.norm(a, 1)
        20.0
        >>> LA.norm(b, 1)
        7.0
        >>> LA.norm(a, -1)
        -4.6566128774142013e-010
        >>> LA.norm(b, -1)
        6.0
        >>> LA.norm(a, 2)
        7.745966692414834
        >>> LA.norm(b, 2)
        7.3484692283495345

        >>> LA.norm(a, -2)
        0.0
        >>> LA.norm(b, -2)
        1.8570331885190563e-016 # may vary
        >>> LA.norm(a, 3)
        5.8480354764257312 # may vary
        >>> LA.norm(a, -3)
        0.0

        Using the `axis` argument to compute vector norms:

        >>> c = np.array([[ 1, 2, 3],
        ...               [-1, 1, 4]])
        >>> LA.norm(c, axis=0)
        array([ 1.41421356,  2.23606798,  5.        ])
        >>> LA.norm(c, axis=1)
        array([ 3.74165739,  4.24264069])
        >>> LA.norm(c, ord=1, axis=1)
        array([ 6.,  6.])

        Using the `axis` argument to compute matrix norms:

        >>> m = np.arange(8).reshape(2,2,2)
        >>> LA.norm(m, axis=(1,2))
        array([  3.74165739,  11.22497216])
        >>> LA.norm(m[0, :, :]), LA.norm(m[1, :, :])
        (3.7416573867739413, 11.224972160321824)

```python
solution = global_criterion_method(car_problem_normalized,
                                           np.inf)
print("variable values are ",solution)
f_global_criterion = car_problem(solution[0],solution[1],solution[2])
print("objective values are ",f_global_criterion)
```

    Optimization terminated successfully    (Exit mode 0)
                Current function value: 0.18028203269723292
                Iterations: 26
                Function evaluations: 69
                Gradient evaluations: 26
    variable values are  [   4.25892988  172.95999244 2500.00213198]
    objective values are  [4.258929879857968, -172.95999243905936, 190387.71146013652]

## A posteriori methods

* A posteriori methods generate a representation of the Pareto optimal solutions, or the complete set of Pareto optimal solutions
* Benefits
  * The solutions can be visualized for problems with 2 or 3 objectives so the decision making is possible
  * When succesful, they give an understanding of the Pareto front
* Drawbacks
  * Approximating the Pareto optimal set often time-consuming
  * Decision making from a large representation may be very difficut

### The weighting method

Based on solving optimization problem
$$
\min_{x\in S} \sum_{i=1}^kw_if_i(x)
$$
for different weights $w_i\geq0$, $i=1,\ldots,k$ such that $\sum_{i=1}^k w_i=1$.

**The idea is to generate weights evenly and then have evenly spread solutions.**

**An optimal solution of the weighted problem is Pareto optimal, if all the weights $w_i$ are $>0$.**

### Application to our problem

```python
import numpy as np
def weighting_method(f,w):
    points = []
    bounds = ((3,10),(50,200),(1000,4000)) #variable bounds of the problem
    start = [7,125,2500] #starting values for the variables
    for wi in w:
        res=minimize(
            #weighted sum
            lambda x: sum(np.array(wi)*np.array(f(x[0],x[1],x[2]))),
            start, method='SLSQP'
            #Jacobian using automatic differentiation
            ,jac=ad.gh(lambda x: sum(np.array(wi)*np.array(f(x[0],x[1],x[2]))))[0]
            #bounds given above
            ,bounds = bounds,options = {'disp':False})
        points.append(res.x)
    return points
```

```python
w = np.random.random ((500,3)) #500 random weights
```

```python
help(np.random.random)
```

    Help on built-in function random:

    random(...) method of numpy.random.mtrand.RandomState instance
        random(size=None)

        Return random floats in the half-open interval [0.0, 1.0). Alias for
        `random_sample` to ease forward-porting to the new random API.

```python
# normalize the weights
wn = w
for i in range(len(w)):
    s = sum(w[i])
    for j in range(2):
        wn[i][j] = w[i][j]/s
```

```python
print(wn)
```

    [[0.30775757 0.13578172 0.83478274]
     [0.43260626 0.33394713 0.39797133]
     [0.13496065 0.4745223  0.74104226]
     ...
     [0.48231806 0.1838959  0.50087782]
     [0.57113275 0.17477249 0.36089137]
     [0.24162153 0.2981989  0.58015565]]

```python
repr = weighting_method(car_problem_normalized,wn)
```

```python
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
def visualize_representation(func,repr):
    f_repr = [func(repri[0],repri[1],repri[2]) for repri in repr]
    print(min(f_repr))
    print(max(f_repr))
    fig = plt.figure()
    ax = fig.add_subplot(111, projection='3d')
    ax.scatter([f[0] for f in f_repr],[f[1] for f in f_repr],[f[2] for f in f_repr])
    return plt
```

```python
visualize_representation(car_problem,repr).show()
```

### Epsilon-constraint method

Based on solving optimization problem

$$
\begin{align}
\min \quad &f_j(x)\\
\text{s.t. }\quad &x\in S\\
&f_i(x)\leq \epsilon_i \text{ for all }i\neq j
\end{align}
$$

for different bounds $\epsilon_i, i\neq j$.

* In other words, select one of the objectives ($f_j$) to be optimized and convert others as constraints by giving them an upper bound ($f_i(x)\leq \epsilon_i$).

* Different PO solutions can be obtained by changing the bounds and/or the objective to be optimized!

**The idea is to generate $\epsilon$ evenly within the bounds of the ideal and nadir vectors and then have evenly spread solutions.**

**A solution $x^*$ is Pareto optimal, if it is the solution to the epsilon constraint problem for all $j=1,\ldots,k$ and $\epsilon = f(x^*)$.**

### Ilustrative example

PO solutions for different upper bounds for $𝑓_2$.

$$
𝜖_1 → \text{no solutions}\\
𝜖_2 → 𝐳^2\\
𝜖_3 → 𝐳^3\\
𝜖_4 → 𝐳^4
$$

### Application to our problem

```python
import numpy as np
from scipy.optimize import minimize
import ad
def e_constraint_method(f,eps,z_ideal,z_nadir):
    points = []
    start = [7,125,2500]
    for epsi in eps:
        bounds = ((3,epsi[0]*(z_nadir[0]-z_ideal[0])+z_ideal[0]), # f1(x) = (c-z1^ideal)/(z1^nadir-z1^ideal) <= eps_1
                  (-1.*(epsi[1]*(z_nadir[1]-z_ideal[1])+z_ideal[1]), # f2(x) = (-p-z2^ideal)/(z2^nadir-z2^ideal) <= eps_2
                   200),(1000,4000)) #Added bounds for two first objectives
        res=minimize(
            #minimize the third objective = Price
            lambda x: f(x[0],x[1],x[2])[2],
            start, method='SLSQP'
            #Jacobian using automatic differentiation
            ,jac=ad.gh(lambda x: f(x[0],x[1],x[2])[2])[0]
            #bounds given above
            ,bounds = bounds,options = {'disp':False})
        if res.success:
            points.append(res.x)
    return points
```

```python
eps = np.random.random((2000,2))
repr_eps = e_constraint_method(car_problem_normalized,eps,z_ideal,z_nadir)
```

```python
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
def visualize_representation(func,repr):
    f_repr = [func(repri[0],repri[1],repri[2]) for repri in repr]
    print(min(f_repr))
    print(max(f_repr))
    fig = plt.figure()
    ax = fig.add_subplot(111, projection='3d')
    ax.scatter([f[0] for f in f_repr],[f[1] for f in f_repr],[f[2] for f in f_repr])
    return plt
```

```python
visualize_representation(car_problem,repr_eps).show()
```

## Comparison of the weighted sum method and the epsilon constraint method

```python
import numpy as np
def weighting_method(f,w):
    points = []
    bounds = ((3,10),(50,200),(1000,4000)) #Bounds of the problem
    start = [7,125,2500]
    for wi in w:
        res=minimize(
            #weighted sum
            lambda x: sum(np.array(wi)*np.array(f(x[0],x[1],x[2]))),
            start, method='SLSQP'
            #Jacobian using automatic differentiation
            ,jac=ad.gh(lambda x: sum(np.array(wi)*np.array(f(x[0],x[1],x[2]))))[0]
            #bounds given above
            ,bounds = bounds,options = {'disp':False})
        points.append(res.x)
    return points
```

```python
w = np.random.random((2000,3)) #500 random weights
wn = w # normalized weights
for i in range(len(w)):
    s = sum(w[i])
    for j in range(3):
        wn[i][j] = w[i][j]/s
repr_ws = weighting_method(car_problem_normalized,wn)
#repr_ws = weighting_method(car_problem_normalized,w)
```

```python
visualize_representation(car_problem,repr_ws).show()
```

```python
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
def visualize_two_representations(func,repr1,repr2):
    f_repr_1 = [func(repr1i[0],repr1i[1],repr1i[2]) for repr1i in repr1]
    f_repr_2 = [func(repr2i[0],repr2i[1],repr2i[2]) for repr2i in repr2]
    fig = plt.figure()
    ax = fig.add_subplot(111, projection='3d')
    #Mark solutions to epsilon constsraint problem using crosses
    ax.scatter([f[0] for f in f_repr_1],[f[1] for f in f_repr_1],
               [f[2] for f in f_repr_1],marker='x')
    #Mark solutions to weighted sum problem using dots
    ax.scatter([f[0] for f in f_repr_2],[f[1] for f in f_repr_2],
               [f[2] for f in f_repr_2],marker='o')
    return plt
```

```python
visualize_two_representations(car_problem,repr_eps,repr_ws).show()
```

**The weighting method can find all the Pareto optimal solutions only, when the objective functions are convex and the feasible set $S$ is convex.**

**The weighting method can produce very unevenly spread Pareto optimal solutions, even when the problem is convex.**

**The epsilon constraint method, however, adds constraints to the problem, which may make it much harder to solve**

## Note
During the last 15-20 years, evolutionary multiobjective optimization (EMO) approaches have become popular in multiobjective optimization. They
* operate on a population of solutions and their aim is to generate an approximation of the whole Pareto front (the set of all Pareto optimal solutions in the objective space),
* are very efficient in solving problems with 2-3 objective functions although they typically use a large number of function evaluations,
* are recently extended to work more efficiently for problems with more than 3 objective functions.

During the last 5-10 years, decision maker preferences are incorporated to the EMO algorithms to 1) improve convergence and 2) find solutions that are preferred by the decision maker.
* You can learn more about these approaches in the courses TIES451 Selected topics in soft computing and TIES598 Nonlinear Multiobjective Optimization

## A priori methods

* A priori methods ask for preferences from the decision maker, and then find the Pareto optimal solution that best matches these preferences
* Benefits
  * If the decision maker knows what he/she wants and understands the preference information asked for, then application is fast
* Drawbacks
  * The decision maker may not know what he/she wants, because he does not know the Pareto optimal solutions
  * The decision maker may not understand how the preferences he/she gives affect the solutions found

## Achievement scalarizing problem

There are multiple versions of the achievement scalarizing problem, but all of them are based on a refence point.

A reference point
$$z^{ref} = (z^{ref}_1,\ldots,z^{ref}_k)$$
contains preferable values (so-called aspiration levels) for the objectives.

Then the achievement scalarizing problem maps this point and a feasible solution to the multiobjective problem to a scalar (i.e., scalarizes it). One of the most commonly used is

$$
\min_{x\in S}\left( \max_{i=1}^k[f_i(x)-z_i^{ref}] +\rho\sum_{i=1}^kf_i(x)\right)
$$

where $\rho>0$ is a small value. The second part is called an augmentation term.

**The solution to the problem is guaranteed to be Pareto optimal**

**Any (properly) Pareto optimal solution can be found with some reference point**

### Application to our car problem

```python
import numpy as np
from scipy.optimize import minimize
import ad
def asf(f,ref,z_ideal,z_nadir,rho):
    bounds = ((3,10),(50,200),(1000,4000)) #Bounds of the problem
    #Normalizing the reference point
    ref_norm = [(refi-z_ideali)/(z_nadiri-z_ideali)
                for (refi,z_ideali,z_nadiri) in zip(ref,z_ideal,z_nadir)]
    def obj(x):
        return np.max(np.array(f(x[0],x[1],x[2]))-ref_norm)\
           +rho*np.sum(f(x[0],x[1],x[2]))
    start = [7,125,2500]
    res=minimize(
        #Objective function defined above
        obj,
        start, method='SLSQP'
        #Jacobian using automatic differentiation
        ,jac=ad.gh(obj)[0]
        #bounds given above
        ,bounds = bounds,options = {'disp':True, 'ftol': 1e-20,
                                    'maxiter': 1000})
    return res
```

```python
rho = 0.000001
#The reference point for the problem
ref =  [4,-60,12000] #To be added at the class
#ref =  z_ideal
res = asf(car_problem_normalized,ref,z_ideal,z_nadir,rho)
print("Solution is ",res.x)
print("Objective function values are ",car_problem(res.x[0],res.x[1],res.x[2]))
print(z_ideal)
```

    Iteration limit reached    (Exit mode 9)
                Current function value: 0.041105710879986095
                Iterations: 1000
                Function evaluations: 10779
                Gradient evaluations: 1000
    Solution is  [   4.27540441  117.54938247 2499.99708315]
    Objective function values are  [4.275404407128466, -117.54938247197143, 54269.04362949268]
    [3.0, -200.0, 5000]

## Interactive methods

* Interactive methods iteratively search for the preferred solution with decision maker and optimization alternating
* Benefits
  * Decision maker gets to learn about
    * the available solutions, and
    * how preferences affect the solutions found
  * Computation is less intensive, because no need to generate a large representation of Pareto optimal solutions
* Drawbacks
  * Needs active involvement from the decision maker
  * If the problem is computationally expensive, then the decision maker may need to wait a long time between solutions

## Interactive methods (cont)
The steps of the general algorithm are the following:
1. Initialize the solution process, e.g., calculate ideal and nadir objective vectors.
2. Generate initial Pareto optimal solution to be used as a current solution.
3. Show the current solution to the decision maker.
4. Ask the decision maker to provide preference information related to the current solution.
5. Generate new solution(s) based on the preference information.
6. Show the solutions generated in step 5 to the decision maker. Ask her/him to select the best solution of those and denote it as the current solution.
7. If the selected current solution is satisfactory for the decision maker, stop. Otherwise, continue from step 4.

## Interactive methods (cont)

**Interactive methods are one of the main research areas here at the Multiobjective optimization research group**

We will demonstrate interactive methods by using the open source DESDEO framework developed at the research group (https://desdeo.it.jyu.fi/). DESDEO includes implementations of several interactive methods.

### The synchronous NIMBUS Method

Introduced by Professors Kaisa Miettinen and Marko Mäkelä in 2006:

*Miettinen, K. and Mäkelä, M. M., Synchronous approach in interactive multiobjective optimization, European Journal of Operational Research, 170: 909-922, 2006*

Is based on classification of objectives into
* those that should be improved as much as possible,
* those that should be improved until a given limit,
* those that are acceptable at the moment,
* those that can be allowed to worsen until a given limit,
* those that are allowed to move freely at the moment.

Synchronous NIMBUS is based on representing this information as four different single-objective optimization problems that are then solved and solutions are shown to the decision maker.

## A note

Reference point method: At each iteration, the decision maker provides a reference point and new solution is generated by using achievement scalarizing problem. Most of the subproblems in the synchronous NIMBUS method are based on this idea.

## Implementations of the NIMBUS method

There also exists a WWW-NIMBUS (https://wwwnimbus.it.jyu.fi/) web implementation (has not been maintained in more than 10 years, but works).

The Synchronous NIMBUS is implemented as a part of IND-NIMBUS (http://ind-nimbus.it.jyu.fi/) software framework.

Recently, development of the DESDEO framework (https://desdeo.it.jyu.fi/) has been started to enable open-source framework for interactive multiobjective optimization (https://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&arnumber=9591595).

## DESDEO framework

We will have a closer look at the DESDEO framework: https://desdeo.it.jyu.fi/

The following example is from https://desdeo-mcdm.readthedocs.io/en/latest/notebooks/synchronous_nimbus.html. We will have a look at it there.

The same example can be found below if you want to try it by yourself.

To install DESDEO you can use:

(!) pip install desdeo

For more information see the GitHub page at https://github.com/industrial-optimization-group/DESDEO.

```python
import numpy as np

import matplotlib.pyplot as plt
from desdeo_problem.Problem import MOProblem
from desdeo_problem.Variable import variable_builder
from desdeo_problem.Objective import _ScalarObjective

def f_1(xs: np.ndarray):
    xs = np.atleast_2d(xs)
    xs_plusone = np.roll(xs, 1, axis=1)
    return np.sum(-10*np.exp(-0.2*np.sqrt(xs[:, :-1]**2 + xs_plusone[:, :-1]**2)), axis=1)

def f_2(xs: np.ndarray):
    xs = np.atleast_2d(xs)
    return np.sum(np.abs(xs)**0.8 + 5*np.sin(xs**3), axis=1)

varsl = variable_builder(
    ["x_1", "x_2", "x_3"],
    initial_values=[0, 0, 0],
    lower_bounds=[-5, -5, -5],
    upper_bounds=[5, 5, 5],
)

f1 = _ScalarObjective(name="f1", evaluator=f_1)
f2 = _ScalarObjective(name="f2", evaluator=f_2)

problem = MOProblem(variables=varsl, objectives=[f1, f2], ideal=np.array([-20, -12]), nadir=np.array([-14, 0.5]))
```

```python
from desdeo_mcdm.utilities.solvers import solve_pareto_front_representation

p_front = solve_pareto_front_representation(problem, step=1.0)[1]

plt.scatter(p_front[:, 0], p_front[:, 1], label="Pareto front")
plt.scatter(problem.ideal[0], problem.ideal[1], label="Ideal")
plt.scatter(problem.nadir[0], problem.nadir[1], label="Nadir")
plt.xlabel("f1")
plt.ylabel("f2")
plt.title("Approximate Pareto front of the Kursawe function")
plt.legend()
plt.show()
```

```python
from desdeo_mcdm.interactive.NIMBUS import NIMBUS

method = NIMBUS(problem, "scipy_de")

classification_request, plot_request = method.start()
```

```python
print(classification_request.content.keys())
```

```python
print(classification_request.content["message"])
```

```python
print(classification_request.content["objective_values"])
```

```python
print(classification_request.content["classifications"])
```

```python
response = {
    "classifications": ["<", ">="],
    "number_of_solutions": 3,
    "levels": [0, -5]
}
classification_request.response = response
```

```python
save_request, plot_request = method.iterate(classification_request)
```

```python
print(save_request.content.keys())
print(save_request.content["message"])
print(save_request.content["objectives"])
```

```python
response = {"indices": [0, 2]}
save_request.response = response

intermediate_request, plot_request = method.iterate(save_request)
```

```python
print(intermediate_request.content.keys())
print(intermediate_request.content["message"])
```

```python
response = {"number_of_desired_solutions": 0, "indices": []}
intermediate_request.response = response

preferred_request, plot_request = method.iterate(intermediate_request)
```

```python
print(preferred_request.content.keys())
print(preferred_request.content["message"])
```

```python
plt.scatter(p_front[:, 0], p_front[:, 1], label="Pareto front")
plt.scatter(problem.ideal[0], problem.ideal[1], label="Ideal")
plt.scatter(problem.nadir[0], problem.nadir[1], label="Nadir")
for i, z in enumerate(preferred_request.content["objectives"]):
    plt.scatter(z[0], z[1], label=f"solution {i}")
plt.xlabel("f1")
plt.ylabel("f2")
plt.title("Approximate Pareto front of the Kursawe function")
plt.legend()
plt.show()
```

```python
response = {"index": 1, "continue": True}
preferred_request.response = response

classification_request, plot_request = method.iterate(preferred_request)
```

```python
response = {
    "classifications": [">=", "<"],
    "number_of_solutions": 4,
    "levels": [-16, -1]
}
classification_request.response = response

save_request, plot_request = method.iterate(classification_request)
```

```python
plt.scatter(p_front[:, 0], p_front[:, 1], label="Pareto front")
plt.scatter(problem.ideal[0], problem.ideal[1], label="Ideal")
plt.scatter(problem.nadir[0], problem.nadir[1], label="Nadir")
for i, z in enumerate(save_request.content["objectives"]):
    plt.scatter(z[0], z[1], label=f"solution {i}")
plt.xlabel("f1")
plt.ylabel("f2")
plt.title("Approximate Pareto front of the Kursawe function")
plt.legend()
plt.show()
```

```python
response = {"indices": [0, 1, 2, 3]}
save_request.response = response

intermediate_request, plot_request = method.iterate(save_request)
```

```python
plt.scatter(p_front[:, 0], p_front[:, 1], label="Pareto front")
plt.scatter(problem.ideal[0], problem.ideal[1], label="Ideal")
plt.scatter(problem.nadir[0], problem.nadir[1], label="Nadir")
for i, z in enumerate(intermediate_request.content["objectives"]):
    plt.scatter(z[0], z[1], label=f"solution {i}")
plt.xlabel("f1")
plt.ylabel("f2")
plt.title("Approximate Pareto front of the Kursawe function")
plt.legend()
plt.show()
```

```python
response = {
    "indices": [3, 4],
    "number_of_desired_solutions": 3,
    }
intermediate_request.response = response

save_request, plot_request = method.iterate(intermediate_request)
```

```python
plt.scatter(problem.ideal[0], problem.ideal[1], label="Ideal")
plt.scatter(problem.nadir[0], problem.nadir[1], label="Nadir")
for i, z in enumerate(save_request.content["objectives"]):
    plt.scatter(z[0], z[1], label=f"solution {i}")
plt.xlabel("f1")
plt.ylabel("f2")
plt.title("Approximate Pareto front of the Kursawe function")
plt.legend()
plt.show()
```

```python
response = {"indices": [1]}
save_request.response = response

intermediate_request, plot_request = method.iterate(save_request)
```

```python
response = {"number_of_desired_solutions": 0, "indices": []}
intermediate_request.response = response

preferred_request, plot_request = method.iterate(intermediate_request)
```

```python
plt.scatter(p_front[:, 0], p_front[:, 1], label="Pareto front")
plt.scatter(problem.ideal[0], problem.ideal[1], label="Ideal")
plt.scatter(problem.nadir[0], problem.nadir[1], label="Nadir")
for i, z in enumerate(preferred_request.content["objectives"]):
    plt.scatter(z[0], z[1], label=f"solution {i}")
plt.xlabel("f1")
plt.ylabel("f2")
plt.title("Approximate Pareto front of the Kursawe function")
plt.legend()
plt.show()
```

```python
response = {
    "index": 6,
    "continue": False,
    }

preferred_request.response = response

print("hello")
print(preferred_request)

stop_request, plot_request = method.iterate(preferred_request)

print(stop_request)
```

```python
print(f"Final decision variables: {stop_request.content['solution']}")

plt.scatter(p_front[:, 0], p_front[:, 1], label="Pareto front")
plt.scatter(problem.ideal[0], problem.ideal[1], label="Ideal")
plt.scatter(problem.nadir[0], problem.nadir[1], label="Nadir")
plt.scatter(stop_request.content["objective"][0], stop_request.content["objective"][1], label=f"final solution")
plt.xlabel("f1")
plt.ylabel("f2")
plt.title("Approximate Pareto front of the Kursawe function")
plt.legend()
plt.show()
```

Lecture 12, Methods for multiobjective optimization 2.ipynb


## SurfinPy

#### Tutorial 2 - Introducing temperature dependence

In tutorial 1 we generated a phase diagram at 0K. However this is not representative of normal conditions. Temperature is an important consideration for materials chemists and we may wish to evaluate the state of a solid electrolyte at the operating temperature or synthesis conditions. In order to overcome this and introduce temperature we need to modify the equation for our surface energy in tutorial 1. This explanation will again be using $TiO_2$

\begin{align}
\gamma_{Surf} & = \frac{1}{2A} \Bigg( E_{TiO_2}^{slab} - \frac{nTi_{Slab}}{nTi_{Bulk}} E_{TiO_2}^{Bulk} \Bigg) - \Gamma_O \mu_O - \Gamma_{H_2O} \mu_{H_2O} - n_O \mu_O (T) - n_{H_2O} \mu_{H_2O} (T)
\end{align}

where A is the surface area, $E_{TiO_2}^{slab}$ is the DFT energy of the slab, $nTi_{Slab}$ is the number of cations in the slab, $nTi_{Bulk}$ is the number of cations in the bulk unit cell, $E_{TiO_2}^{Bulk}$ is the DFT energy of the bulk unit cell and

\begin{align}
\Gamma_O & = \frac{1}{2A} \Bigg( nO_{Slab} - \frac{nO_{Bulk}}{nTi_{Bulk}}nTi_{Slab}  \Bigg) ,
\end{align}

\begin{align}
\Gamma_{H_2O} & = \frac{nH_2O}{2A} ,
\end{align}

$nO_{Slab}$ is the number of anions in the slab, $nO_{Bulk}$ is the number of anions in the bulk, $nH_2O$ is the number of adsorbing water molecules and $n_O$ is the number of defective oxygen. $\Gamma_O$ / $\Gamma_{H_2O}$ is the excess oxygen / water at the surface and $\mu_O$ / $\mu_{H_2O}$ is the oxygen / water chemcial potential. $\mu_{H_2O} $(T) / $\mu_O (T)$ are terms to correct the chemcial potential of oxygen and water based on thermochemical data from the NIST_JANAF database, making the chemical potential a temperature dependent term.

\begin{align}
\mu_O (T) &  = \frac{1}{2} \mu_O (T) (0 K , DFT) +  \frac{1}{2} \mu_O (T) (0 K , EXP) +  \frac{1}{2} \Delta G_{O_2} ( \Delta T, Exp)
\end{align}

where $\mu_O$ (T) (0 K , DFT) is the 0K free energy of an isolated oxygen moleculeevaluated with DFT, $\mu_O$ (T) (0 K , EXP) is the 0 K experimental  Gibbs energy for oxygen gas and $\Delta$ $G_{O_2}$ ( $\Delta$ T, Exp) is the Gibbs energy defined at temperature T as

\begin{align}
\Delta G_{O_2} ( \Delta T, Exp) &  = \frac{1}{2} [H(T, {O_2}) -  H(0 K, {O_2})] -  \frac{1}{2} T[S(T, {O_2}])
\end{align}

```python
from surfinpy import mu_vs_mu
from surfinpy import utils as ut
from surfinpy import data
```

In order to calculate our $\Delta$ $G_{O_2}$ ( $\Delta$ T, Exp) values we need to use experimental data from the NIST-JANAF database. As a user you need to download the tables for the species you are interested in ( In our case oxygen and water). Surfinpy has a function that can read this data, assuming it is in the correct format and calculate the temperature correction for you. Provide the path to the file and the temperature that you want as an index.

```python
Oxygen_exp = ut.fit_nist("O2.txt")[298]
Water_exp = ut.fit_nist("H2O.txt")[298]
```

-9.08 is the DFT energy of an oxygen molecule, -0.86 is the zero point energy and Oxygen_exp is the experimental free energy at 298 K.

```python
Oxygen_corrected = (-9.08 + -0.86 + Oxygen_exp) / 2
Water_corrected = -14.84 + 0.55 + Water_exp
print(Oxygen_corrected)
print(Water_corrected)
```

    -5.2427609629166
    -14.77234481554258

Oxygen_corrected and Water_corrected are now temperature dependent terms corresponding to a temperature of 298 K. The resulting phase diagram will now be at a temperature of 298 K.

```python
bulk = data.ReferenceDataSet(cation = 1, anion = 2, energy = -780.0, funits = 4)

pure = data.DataSet(cation = 24, x = 48, y = 0, area = 60.0, energy = -575.0, label = "Stoich", nspecies = 1)
H2O = data.DataSet(cation = 24, x = 48, y = 2, area = 60.0, energy = -612.0, label = "1 Water", nspecies = 1)
H2O_2 = data.DataSet(cation = 24, x = 48, y = 4, area = 60.0, energy = -640.0, label = "2 Water", nspecies = 1)
H2O_3 = data.DataSet(cation = 24, x = 48, y = 8, area = 60.0, energy = -676.0, label = "3 Water", nspecies = 1)
Vo = data.DataSet(cation = 24, x = 46, y = 0, area = 60.0, energy = -558.0, label = "Vo", nspecies = 1)
H2O_Vo_1 = data.DataSet(cation = 24, x = 46, y = 2, area = 60.0, energy = -594.0, label = "Vo + 1 Water", nspecies = 1)
H2O_Vo_2 = data.DataSet(cation = 24, x = 46, y = 4, area = 60.0, energy = -624.0, label = "Vo + 2 Water", nspecies = 1)
H2O_Vo_3 = data.DataSet(cation = 24, x = 46, y = 6, area = 60.0, energy = -640.0, label = "Vo + 3 Water", nspecies = 1)
H2O_Vo_4 = data.DataSet(cation = 24, x = 46, y = 8, area = 60.0, energy = -670.0, label = "Vo + 4 Water", nspecies = 1)

data = [pure, Vo, H2O, H2O_Vo_1, H2O_2, H2O_Vo_2, H2O_3, H2O_Vo_3,  H2O_Vo_4]

```

```python
deltaX = {'Range': [ -12, -6],  'Label': 'O'}
deltaY = {'Range': [ -19, -12], 'Label': 'H_2O'}
```

```python
system =  mu_vs_mu.calculate(data, bulk, deltaX, deltaY, x_energy=Oxygen_corrected, y_energy=Water_corrected)
```

```python
system.plot_phase(temperature=298, set_style="fast", colourmap="RdBu")
```

```python

```

```python

```