# **Otimização de Processos (COQ897)**
# *Prof. Argimiro R. Secchi*

$\
$

Terceira Lista de Exercícios - 2020

$\
$

***José Rodrigues Torraca Neto***

$\
$

**7)** Considerando uma planta projetada para a produção de uma variedade de produtos, conforme ilustrado na figura abaixo:

A engenheira Leia ficou encarregada de maximizar o lucro da unidade, limitado à capacidade operacional e sem deixar de atender à demanda do mercado. Os dados de processo econômicos por ela obtidos estão dispostos na tabela abaixo:

**(a)** Qual foi a formulação do problema de otimização elaborado pela Eng. Leia?

 **(b)** Qual foi a solução obtida?

 **(c)** Qual seria a solução se não houvesse demanda mínima?

$\\
$

## ***Solução:***

***(a)*** Primeiro, temos que criar um modelo do processo, que pode ser obtido a partir das equações de balanço de massa.

Definimos $x_{1}, x_{2}, x_{3}$
como as vazões mássicas de entrada de $A$ para cada processo.

Da mesma forma, sejam $x_{4}, x_{5}, x_{6}$ as vazões de $B$ e $x_{7}$ a vazão de $C$.

Definindo também as vazões mássicas dos produtos: $x_{8}, x_{9}, x_{10}$ , respectivamente para $E, F, G$.

Sejam $x_{11}, x_{12}$ as quantidades totais de $A$ e $B$ usadas como reagentes (o total de $C$
é o mesmo que $x_{7}$).

Assim, temos um total de 12 variáveis.

As restrições de igualdade lineares, do equilíbrio de massa, que representam o processo são:

>$A=x_{11}=x_{1}+ x_{2}+ x_{3} \tag{1} $
>
>$B=x_{12}=x_{4}+ x_{5}+ x_{6} \tag{2} $
>
>$x_{1}= \frac{2}{3} x_{8} = 0,667 x_{8} \tag{3} $
>
>$x_{2}= \frac{2}{3} x_{9} = 0,667 x_{9} \tag{4} $
>
>$x_{3}= \frac{1}{2} x_{10} = 0,5 x_{10} \tag{5} $
>
>$x_{4}= \frac{1}{3} x_{8} = 0,333 x_{8} \tag{6} $
>
>$x_{5}= \frac{1}{3} x_{9} = 0,333 x_{9} \tag{7} $
>
>$x_{6}= \frac{1}{6} x_{10} = 0,167 x_{10} \tag{8} $
>
>$x_{7}= \frac{1}{3} x_{10} = 0,333 x_{10} \tag{9} $

Com 12 variáveis e 9 restrições de igualdade linearmente independentes, temos 3 graus de liberdade que podem ser usados para maximizar o lucro.

A receita total diária em reais da planta é obtida a partir dos preços de venda:

>$Receita(R\$ /dia): 4,00E + 3,30F + 3,80G$

Os custos operacionais diários em reais incluem:

>$Custo \ da \ matéria-prima \ bruta: 1,50A + 2,00B + 2,50C \\
Custo \ de \ processamento: 1,50E + 0,50F + 1,00G \\
Custo \ total \ (R\$ /dia): 1,50A + 2,00B + 2,50C + 1,50E + 0,50F + 1,00G
$

A função objetivo que representa o lucro diário $f(x)$ é obtida subtraindo-se a receita diária pelo custo total diário:

>$f(x)= 2,5E + 2,80F + 2,80G - 1,50 A - 2,00 B - 2,50C$
>
>$f(x)= 2,5x_{8} + 2,80x_{9} + 2,80x_{10} - 1,50 x_{11} - 2,00 x_{12} - 2,50 x_{7}
$

As seis variáveis na função objetivo do lucro são restringidas pelos balanços de massa:

>$x_{11}= 0,667 x_{8} +0,667 x_{9}+0,5 x_{10} $
>
>$x_{12}= 0,333 x_{8} +0,333 x_{9}+0,167 x_{10} $
>
>$x_{7}= 0,333 x_{10} $

Substituindo essas três últimas equações em $f(x)$, temos:

>$f(x)= 0,8335x_{8} + 1,1335x_{9} + 0,8835x_{10}
$

Como é um problema de maximização, podemos considerar a minimização de $-f(x)$:

>$f(x)= -0,8335x_{8} - 1,1335x_{9} - 0,8835x_{10} \tag{I}$

Com as seguintes restrições de desigualdade:

>$0\leq x_{11}\leq 40000 $
>
>$0\leq x_{12}\leq 30000 $
>
>$0\leq x_{7}\leq 25000 $

Substituindo novamente as 3 equações dos balanços de massa nas inequações acima, temos:

>$0\leq 0,667 x_{8} +0,667 x_{9}+0,5 x_{10}\leq 40000 \tag{II}$
>
>$0\leq 0,333 x_{8} +0,333 x_{9}+0,167 x_{10}\leq 30000 \tag{III}$
>
>$0\leq 0,333 x_{10}\leq 25000 \tag{IV}$

Finalmente, temos as últimas restrições de desigualdade, na produção de $E, F,
 G$; a fim de satisfazer a demanda de mercado:

>$x_{8}\geq 20000 \tag{V} $
>
>$x_{9}\geq 15000 \tag{VI}$
>
>$x_{10}\geq 25000 \tag{VII}$

***Resposta: (a)*** A formulação do problema consiste na minimização da função objetivo $\color{orange}{(I)}$ com as restrições de desigualdade $\color{orange}{(II), (III), (IV), (V), (VI), (VII)}$.

$\\
$

***(b)*** O problema de otimização descrito no item anterior compreende uma função objetivo linear e
restrições lineares, portanto, a programação linear é a melhor técnica para resolvê-lo.

Aqui, iremos utilizar o método ***Simplex***, por meio do pacote ***Scipy***, por se tratar de um problema com pequenas dimensões e baixo número de restrições.

O problema consiste em minimizar:

>$ min_{x} f(x)= -0,8335x_{8} - 1,1335x_{9} - 0,8835x_{10}$

com as restrições:

>$0,667 x_{8} +0,667 x_{9}+0,5 x_{10}\leq 40000$
>
>$0,333 x_{8} +0,333 x_{9}+0,167 x_{10}\leq 30000$
>
>$0,333 x_{10}\leq 25000$
>
>$x_{8}\geq 20000$
>
>$x_{9}\geq 15000$
>
>$x_{10}\geq 25000$

Note que as restrições relativas à $x_{10}$ se resumem aos seguintes limites inferiores e superiores ("***bounds***"):

>$25000 \leq x_{10} \leq 75075,08$

O problema também já está na forma ***padrão*** de programação linear, ou seja, suas variáveis já estão restritas a valores positivos.

Então, podemos aplicar o método Simplex diretamente, com o auxílio da função ***linprog*** do módulo ***scipy.optimize***:

***OBS:*** A função linprog () resolve apenas problemas de minimização (não maximização) e não permite restrições de desigualdade com o sinal maior ou igual (≥).
Ela também leva os limites (zero a infinito positivo) por padrão.

```
#Coeficientes "c" da função objetivo:

c = [-0.8335, -1.1335, -0.8835]

#Matriz "A" das restrições:

A = [[0.667, 0.667, 0.5], [0.333, 0.333, 0.167]]

#Vetor "b" das restrições:

b = [40000, 30000]

#Limites inferiores e superiores ("bounds") das variáveis:

x8_bounds = (20000, None)
x9_bounds = (15000, None)
x10_bounds = (25000, 75075.8)

from scipy.optimize import linprog

opt = linprog(c, A_ub=A, b_ub=b, bounds=[x8_bounds, x9_bounds, x10_bounds], method='revised simplex')

print(opt)
```

         con: array([], dtype=float64)
         fun: -63101.885
     message: 'Optimization terminated successfully.'
         nit: 2
       slack: array([    0.  , 12782.23])
      status: 0
     success: True
           x: array([20000., 15000., 33310.])

A função .linprog () retorna uma estrutura de dados com os seguintes atributos:

*   con: são os resíduos das restrições de igualdade.
*   fun: é o valor da função objetivo no ponto ótimo (se encontrado).
*   message é o status da solução.
*   nit: é o número de iterações necessárias para concluir a otimização.
*   slack: são os valores das variáveis de folga, ou seja, as diferenças entre os valores dos lados esquerdo e direito das restrições.
*   status: é um número inteiro entre 0 e 4 que mostra o status da solução, como 0 para quando a solução ótima for encontrada.
*   success: é um booleano que mostra se a solução ótima foi encontrada.
*   x: é uma matriz NumPy contendo os valores ótimos das variáveis de decisão.

```
#Podemos acessar esses valores separadamente:

print(-opt.fun)     #Aqui já invertemos o sinal da função objetivo (maximização)

print(opt.success)

print(opt.x)
```

    63101.885
    True
    [20000. 15000. 33310.]

***Resposta: (b)*** A distribuição ótima de produção é: $20000$ $kg/dia$ de $E$, $15000$ $kg/dia$ de $F$ e $33310$ $kg/dia$ de $G$. O lucro máximo é de $63101,885$ $R\$/dia$.

$\\
$

***(c)*** O problema é similar ao item anterior, porém sem os limites inferiores ("***inferior bounds***") para as variáveis:

***OBS:*** Mesmo que não existam limites para a variável $x_{i}$, nós podemos especificar explicitamente os limites $0 \leq x_{i}\leq \infty $ ("None") para todas as variáveis. Como observado anteriormente, o padrão do método é que todas as variáveis sejam positivas.

```
#Coeficientes "c" da função objetivo:

c = [-0.8335, -1.1335, -0.8835]

#Matriz "A" das restrições:

A = [[0.667, 0.667, 0.5], [0.333, 0.333, 0.167]]

#Vetor "b" das restrições:

b = [40000, 30000]

#Limites inferiores e superiores ("bounds") das variáveis:

x8_bounds = (0, None)
x9_bounds = (0, None)
x10_bounds = (0, 75075.8)

from scipy.optimize import linprog

opt = linprog(c, A_ub=A, b_ub=b, bounds=[x8_bounds, x9_bounds, x10_bounds], method='revised simplex')

print(opt)
```

         con: array([], dtype=float64)
         fun: -70513.56277826086
     message: 'Optimization terminated successfully.'
         nit: 2
       slack: array([    0.        , 16233.13705217])
      status: 0
     success: True
           x: array([    0.        ,  3691.30434783, 75075.8       ])

```
#Podemos acessar esses valores separadamente:

print(-opt.fun)

print(opt.success)

print(opt.x)
```

    70513.56277826086
    True
    [    0.          3691.30434783 75075.8       ]

***Resposta: (c)***  A distribuição ótima de produção seria: $0$ $kg/dia$ de $E$; $3691,3$ $kg/dia$ de $F$ e $75075,8$ $kg/dia$ de $G$. O lucro ótimo seria de $70513,6$ $R\$/dia$.

$\\
$

$\\
$
**8)** Determine o trabalho mínimo do sistema de compressão abaixo, considerando
compressão adiabática de gás ideal:

>$
W=\frac{kRT_{1}}{k-1}\left [ \left ( \frac{p_{2}}{p_{1}}\right)^{\frac{k-1}{k}} + \left ( \frac{p_{3}}{p_{2}}\right)^{\frac{k-1}{k}}+ \left ( \frac{p_{4}}{p_{3}}\right)^{\frac{k-1}{k}} -3\right ]
$

Dados $k = 1,4,\ p_{1} = 100\ kPa, \ p_{4} = 1000 \ kPa$ e  $ T_{1} = 300 K.$

 (a) Esboce a função objetivo e
suas curvas de níveis em função de $p_{2}$ e $p_{3}.$

(b) Utilize os métodos de Rosenbrock,
Powell, steepest descent, gradiente conjugado, BFGS e DFP para obter a solução ótima
e compare os resultados obtidos.

$\\
$

## ***Solução:***

***(a)*** Queremos determinar as pressões ótimas $p_{2}$ e $p_{3}$, para minimizar a função objetivo $W$
mantendo $p_{1}$ e $p_{4}$ fixos.

Lembrando que $R=8,314462 \ \ L · kPa · K^{−1} · mol^{−1}$

Substituindo as constantes na expressão de $W$, e chamando $W$ de $f(x)$; $p_{2}$ e $p_{3}$ de $x_{1}$ e $x_{2}$ respectivamente, temos o seguinte problema de otimização :

>$
\underset{x_{1},x_{2}}{min} f(x_{1},x_{2})=8730,1851
\left [ \left ( \frac{x_{1}}{100}\right)^{0,2857
} + \left ( \frac{x_{2}}{x_{1}}\right)^{0,2857
}+ \left ( \frac{1000}{x_{2}}\right)^{0,2857
} -3\right ]
$

```
import numpy as np
import scipy.integrate
import matplotlib.pyplot as plt
%matplotlib inline
from scipy.optimize import minimize

#Verificando a forma da superfície descrita pela função objetivo:

from matplotlib import cm

x1 = np.linspace(150, 300, 50)
x2 = np.linspace(350, 500, 50)
X, Y = np.meshgrid(x1, x2)

Z = 8730.1851*((X/100)**0.2857 + (Y/X)**0.2857 + (1000/Y)**0.2857 - 3)

fig, ax = plt.subplots(subplot_kw={'projection': '3d'})
ax.plot_surface(X, Y, Z, cmap=cm.rainbow)

ax.set_xlabel('$x1$')
ax.set_ylabel('$x2$')
ax.set_zlabel('$f(x1,x2)$');
```

```
#Plotando superfície de contorno (com colorbar):

plt.contourf(X, Y, Z, 100, cmap='RdGy')
plt.colorbar();
plt.show()
```

```
#Plotando superfície de contorno (com colorbar) - com labels:

contours = plt.contour(X, Y, Z, 5, colors='black')
plt.clabel(contours, inline=True, fontsize=8)

plt.imshow(Z, extent=[150, 300, 350, 500], origin='lower',
           cmap='rainbow', alpha=0.5)
plt.colorbar();
```

```
#Plot superfície de contorno (padrão) - com labels:

fig, ax = plt.subplots()
CS = ax.contour(X, Y, Z, [6450,6500,6550,6600], cmap='inferno')
ax.clabel(CS, inline=1, fontsize=10)
ax.set_title('Superfície de contorno com labels')
ax.set_xlabel('$x1$')
ax.set_ylabel('$x2$')
```

$\\
$

***(b)*** Podemos dividir os métodos mencionados no problema em 2 tipos:

- Métodos diretos: não fazem uso da derivada de f(x) **["Rosenbrock" e "Powell"]**.
- Métodos indiretos: fazem uso da derivada de f(x) **["steepest descent", "gradiente conjugado"]**; e em alguns casos também da Hessiana de f(x) **["BFGS" e "DFP"]**.

Os métodos diretos também são conhecidos como DFO ("Derivative Free Optimization"), e os indiretos também são conhecidos como analíticos.

Todos os métodos pedidos serão implementados em algoritmos na linguagem ***Python***, e os indiretos também com o auxílio de um pacote gerenciador de "tabelas/planilhas" conhecido como "***pandas***".

Para podermos fazer comparações coerentes entre os métodos, todos devem possuir o mesmo método de busca de direção ("***linesearch***"), que aqui será considerado como o método da seção áurea ("***golden search***").

Além disso, todos os métodos possuirão um critério de parada, com o mesmo valor para a tolerância (épsilon) de $1\cdot 10^{-9}$. Não teremos um critério de parada de número máximo de iterações, porque queremos comparar o número de iterações de cada método; mas esse critério também pode ser facilmente imposto com uma ou duas linhas de código se necessário.

Todos os métodos também partirão do ponto inicial arbitrário: $x=(100,100).$ Além disso, os método de Powell e BFGS também serão executados utilizando o pacote Scipy, a fim de comparar os métodos diretos e indiretos respectivamente, com os nossos métodos já implementados.

Finalmente, as informações mais relevantes de cada método serão dispostas em um dataframe, em conjunto com uma visualização gráfica do "caminho" do método sobre as curvas de níveis da função objetivo.

1º Método de otimização direto - ***Rosenbrock***:

```
# Rosenbrock é um método de busca multivariável parecido com a fase de exploração
# do método de Hooke  &  Jeeves.

# Entretanto,  ao  invés  de  continuamente explorar  nas  direções  dos  eixos  coordenados,
# são  construídas  novas  direções ortogonais,  usando  o  procedimento  de  Gram-Schmidt,
# baseadas  nos  tamanhos  dos passos das direções bem sucedidas.

import numpy as np
import pandas as pd
from numpy import linalg as LA
import matplotlib.pyplot as plt
import math
import sympy
from sympy.utilities.lambdify import lambdify

#═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬

# Primeiro, vamos simplificar a expressão da função objetivo em termos simbólicos,
# a ajuda do pacote Sympy.

# Extraindo a função objetivo f, em termos simbólicos (.sympy.lambdify):

def f_x(f_expression, values):
    f = lambdify((v[0],v[1]), f_expression)
    return f(values[0],values[1])

v = sympy.Matrix(sympy.symbols('x[0] x[1]'))

# Criando a função objetivo do exercício (8):

f_sympy8 = 8730.1851*((v[0]/100)**0.2857 + (v[1]/v[0])**0.2857 + (1000/v[1])**0.2857 - 3)

# Alocando-a como "f":

f = f_sympy8

# Extraindo a função:

f = f_x(f, v)
print('A função objetivo é: ',f)
print()

#═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬
```

    A função objetivo é:  2342.1971697181*x[0]**0.2857 + 8730.1851*(x[1]/x[0])**0.2857 + 62823.6915040394*(1/x[1])**0.2857 - 26190.5553

```
import numpy as np
import math
import matplotlib.pylab as pl

# Inicializando o contador externamente

count = 0

# Definindo uma função que dá a norma de um vetor (eleva ao quadrado e tira a raiz de cada elemento i)

def norm(s1):                      # Na realidade já existe o método np.linalg(LA).norm que é idêntico
    normas = 0
    for i in range(len(s1)):
        normas += s1[i]**2
    return math.sqrt(normas)

# Definindo um contador que entrará junto com a função fun(x), que dirá quantas vezes fun(x) foi avaliada

def incCount():
    global count
    count = count+1

# Definindo uma função que retorna um vetor "b" de coordenadas perpendicular a um vetor "a" de entrada
# (são arrays 2x2)

def perpendicular(a):
    b = np.empty_like(a)
    b[0] = -a[1]
    b[1] = a[0]
    return b

# Definindo a função fun(x)

def fun(x):
    incCount()
    return 8730.1851*((x[0]/100)**0.2857 + (x[1]/x[0])**0.2857 + (1000/x[1])**0.2857 - 3)

#═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬

# Aqui, vamos criar uma função plot para visualizar o caminho que o método realiza sobre as curvas de níveis da função objetivo.

def plot(points):

    n = 256
    x = np.linspace(50, 300, n)
    y = np.linspace(50, 600, n)
    X, Y = np.meshgrid(x, y)

    xs = []
    ys = []

    pl.contourf(X, Y, fun([X, Y]), 8, alpha=.75, cmap='jet')
    C = pl.contour(X, Y, fun([X, Y]), 8, colors='black', linewidth=.5)

    for i in range(len(points)):
        xs.append(points[i][0])
        ys.append(points[i][1])

    pl.plot(xs, ys, marker='o', linestyle='--', color='r', label='Square')

#═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬

# Definindo o método de Rosenbrock ("rosen")

def rosen(x0, lmb, epsilon1, epsilon2, alpha, beta):

# x0 : ponto inicial, definido como uma array de entrada [x, y]

# lmb : delta x, array que pode ser definida como [0.1, 0.1]

# epsilon1, epsilon2 : erros de estimação ~ tol

# epsilon1: ||x^2 - x^1|| / ||x^2||

# epsilon2: |f(x^2) - f(x^1)| / |x^2|

# alpha, beta - coeficientes para aumentar/diminuir cada passo, respectivamente.
# Podem ser definidos como (3.0, -0.5)

# success = array para salvar respostas YES/NO

    start = x0
    s1 = [1, 0]                   # vetor s1 (ou direção d1), inicialmente [1,0]
    s2 = [0, 1]                   # vetor s2 (ou direção d2), inicialmente [0,1]
    N = 0                         # N: nº de iterações
    success1 = []
    success2 = []
    points = []
    points.append(x0)
    iterations = 0

    temp = []

    while True:
        temp = x0
        x1 = [x0[0] + lmb[0]*s1[0], x0[1] + lmb[0]*s1[1]]

        if fun(x1) < fun(x0):
            success1.append("Y")
            lmb[0] *= alpha
            x0 = x1[:]

        elif fun(x1) > fun(x0):
            success1.append("N")
            lmb[0] *= beta

        x2 = [x0[0] + lmb[1]*s2[0], x0[1] + lmb[1]*s2[1]]

        if fun(x2) < fun(x0):
            success2.append("Y")
            lmb[1] *= alpha
            x0 = x2[:]
        elif fun(x2) > fun(x0):
            success2.append("N")
            lmb[1] *= beta

        if success1[len(success1)-1] == "N" and success2[len(success2)-1] == "N" and "Y" in success1 and "Y" in success2:
            print("Our stop point: [%8.5f, %8.5f]" % (x0[0], x0[1]))
            s1 = [x0[0] - start[0], x0[1] - start[1]]
            print("S1" + str(s1))
            norma = norm(s1)
            s1[0] /= norma
            s1[1] /= norma
            s2 = perpendicular(s1)
            print("F(X1) = %8.5f" % fun(x0))

            error_point = norm([x0[0] - start[0], x0[1] - start[1]])/norm(x0)
            error_func = abs(fun(x0) - fun(start))/abs(fun(start))

            print("||x^2 - x^1|| / ||x^2|| = %8.5f" % error_point)
            print("|f(x^2) - f(x^1)| / |x^2| = %8.5f" % error_func)

            if error_point < epsilon1 or error_func < epsilon2 and N>1:
                print("DONE")
                print("N = %3d" % N)
                break

            points.append(x0)
            start = x0[:]
            success1 = []
            success2 = []
            N+=1

    plot(points)
    print("Nº de avaliações da função objetivo = " + str(count))
    print("Ponto ótimo = " + str(x0))
    print("Função objetivo no ótimo = %8.5f" % fun(x0))

def main():
    x0 = [100, 100]
    rosen(x0, [0.1, 0.1], 1e-9, 1e-9, 3.0, -0.5)

if __name__ == '__main__':
   main()
```

2º Método de otimização direto - ***Powell***:

```
# Powell é  um  método  de  busca  multivariável  ao  longo  de  direções  conjugadas

# Duas direções d1  e d2 são ditas conjugadas entre si, com respeito a uma matriz H, se:
# d1.T  H d2  = 0

# A  ortogonalidade  é  um  caso  particular  de  conjugância,  onde  H  =  I.

# O método de Powell também permite que a função seja não-diferenciável, já que não usa suas derivadas.

# No entanto, ele usa uma informação adicional em relação ao Rosenbrock, que é o alfa (tamanho do passo na direção d)

# obtido por um método de otimização monovariável, no caso aqui sendo o método da seção áurea ("golden search").

#═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬
```

***ADICIONAL:*** Método de otimização direto - ***Powell*** por meio do Scipy.optimize.minimize:

```
import numpy as np
from scipy.optimize import minimize

x0 = np.array([100, 100])

# Definindo a função objetivo f(x):

def f(x):
    return 2342.1971697181*x[0]**0.2857 + 8730.1851*(x[1]/x[0])**0.2857 + 62823.6915040394*(1/x[1])**0.2857 - 26190.5553

res = minimize(f, x0, method='powell', options={'ftol': 1e-9, 'disp': True})

res.x
```

    Optimization terminated successfully.
             Current function value: 6421.490224
             Iterations: 5
             Function evaluations: 175

    /usr/local/lib/python3.6/dist-packages/ipykernel_launcher.py:9: RuntimeWarning: invalid value encountered in double_scalars
      if __name__ == '__main__':

    array([215.44346647, 464.158889  ])

1º Método de otimização indireto - ***Gradiente (Steepest Descent)***:

```
# Primeiro, vamos implementar o método steepest descent em python (completo com visualização gráfica):

# Lembrando que o método steepest descent é um método indireto, ou seja, ele faz uso da derivada de f (df_x).

# Nesse bloco de código, vamos utilizar métodos simbólicos para o cálculo dos gradientes (sympy).

# Além disso, outros métodos como Newton fazem uso da Hessiana explicitamente, então é conveniente calcular aqui também.

import numpy as np
import pandas as pd
from numpy import linalg as LA
import matplotlib.pyplot as plt
import math
import sympy
from sympy.utilities.lambdify import lambdify

# Extraindo a função objetivo f, em termos simbólicos (.sympy.lambdify):

def f_x(f_expression, values):
    f = lambdify((v[0],v[1]), f_expression)
    return f(values[0],values[1])

# Extraindo os gradientes de f:

def df_x(f_expression, values):
    df1_sympy = np.array([sympy.diff(f_expression, i) for i in v])       #derivadas de 1ª ordem
    dfx_0 = lambdify((v[0],v[1]), df1_sympy[0])                          #derivada em relação à x_0
    dfx_1 = lambdify((v[0],v[1]), df1_sympy[1])                          #derivada em relação à  x_1
    evx_0 = dfx_0(values[0], values[1])                                  #calculando o gradiente nos valores dados
    evx_1 = dfx_1(values[0], values[1])
    return(np.array([evx_0,evx_1]))

# Extraindo a Hessiana:

def hessian(f_expression):
    df1_sympy = np.array([sympy.diff(f_expression, i) for i in v])              #derivadas de 1ª ordem
    hessian = np.array([sympy.diff(df1_sympy, i) for i in v])                   #Hessiana
    return(hessian)

#═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬

# Aqui, vamos criar 2 funções para visualizar o caminho que cada método realiza sobre as curvas de níveis da função objetivo.

# Função para criar um plot de curvas de níveis:

def contour(sympy_function):
    x = np.linspace(50, 300, 100)
    y = np.linspace(50, 600, 100)
    x, y = np.meshgrid(x, y)
    func = f_x(sympy_function, np.array([x,y]))
    return plt.contour(x, y, func)

# Função para plotar com o caminho do algoritmo:

def contour_travel(x_array, sympy_function):
    x = np.linspace(50, 300, 100)                         #x-axis
    y = np.linspace(50, 600, 100)                         #y-axis
    x, y = np.meshgrid(x, y)                               #criando o grid x & y
    func = f_x(sympy_function, np.array([x,y]))
    plt.contour(x, y, func)
    plot = plt.plot(x_array[:,0],x_array[:,1],'x-')
    return (plot)

v = sympy.Matrix(sympy.symbols('x[0] x[1]'))

#═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬

# Criando a função objetivo do exercício (8):

f_sympy8 = 8730.1851*((v[0]/100)**0.2857 + (v[1]/v[0])**0.2857 + (1000/v[1])**0.2857 - 3)

# Alocando-a como "f":

f = f_sympy8

# Extraindo a função:

f = f_x(f, v)
print('A função objetivo é: ',f)
print()

# Encontrando o gradiente da função:

df = df_x(f, v)
print('O gradiente da função é: ',df)
print()

# Encontrando a Hessiana da função:             # A etapa da Hessiana é opcional,
                                                # porque não vamos usá-la explicitamente nos métodos aqui considerados,
hess_f = hessian(f)                             # que fazem aproximações de H.
print('A Hessiana da função é: ',hess_f)
print('')

# Plotando as curvas de níveis:

con = contour(f)
```

```
# Inicializando o contador externamente

count = 0

# Definindo o contador que entrará junto com a função fun(x), que dirá quantas vezes fun(x) foi avaliada

def incCount():
    global count
    count = count+1

# Definindo a função objetivo fo(x1,x2):

def fo(x1,x2):
    incCount()
    return 2342.1971697181*x1**0.2857 + 8730.1851*(x2/x1)**0.2857 + 62823.6915040394*(1/x2)**0.2857 - 26190.5553

# Definindo o gradiente da função:

def gradf(x1,x2):
    return np.matrix([[669.165731388461*x1**(-0.7143) - 2494.21388307*(x2/x1)**0.2857/x1],
                      [2494.21388307*(x2/x1)**0.2857/x2 - 17948.7286627041*(1/x2)**0.2857/x2]])

# Visualização gráfica da função para certificar que está correta:

from mpl_toolkits.mplot3d import Axes3D
from matplotlib import cm
from matplotlib.ticker import LinearLocator, FormatStrFormatter

fig = plt.figure()
ax = fig.gca(projection='3d')

X = np.linspace(150, 300, 50)
Y = np.linspace(350, 500, 50)
X, Y = np.meshgrid(X, Y)
Z = fo(X,Y)

surf = ax.plot_surface(X, Y, Z,cmap=cm.coolwarm)
ax.set_xlabel('$x1$')
ax.set_ylabel('$x2$')
ax.set_zlabel('$f(x1,x2)$');

plt.show()

#═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬

# Definindo o método da seção áurea ("golden search") customizado para minimizar o "alfa" (passo). "(ak, bk)" é o intervalo.

# "length_uncertainty" é a incerteza admitida para a solução. "Y" é o ponto inicial e "d" a direção de busca:

def golden_search(ak, bk,length_uncertainty, Y, d):

    k=1
    termination=True

    # Definindo as direções X1 e X2 e o valor do ponto inicial:

    x1_in=float(Y[0])
    x2_in=float(Y[1])
    d1=float(d[0])
    d2=float(d[1])

    # Inicialização. Calculando alfa, u e os valores das funções:

    gama=((-1+math.sqrt(5))/2)                      #gama é o "nº áureo" (0,618)
    alfak=gama*ak+(1-gama)*bk
    uk=(1-gama)*ak+gama*bk

    # Calculando os valores das funções usando o ponto e a direção dados, e o alfa e u calculados:

    falfak=fo(x1_in+alfak*d1, x2_in+alfak*d2)
    fuk=fo(x1_in+uk*d1,x2_in+uk*d2 )

    while termination:

        # Definindo novos valores para o intervalo:

        if falfak<fuk:
            ak=ak
            bk=uk
            uk=alfak
            alfak=gama*ak+(1-gama)*bk
            fuk=falfak
            falfak=fo(x1_in+alfak*d1, x2_in+alfak*d2)

        else:
            ak=alfak
            bk=bk
            alfak=uk
            uk=(1-gama)*ak+gama*bk
            falfak=fuk
            fuk=fo(x1_in+uk*d1,x2_in+uk*d2 )

        # Checando o critério de parada:

        if (bk-ak)<length_uncertainty:

            # A solução ótima está dentro do intervalo [ak,bk].

            # Escolhendo o ponto do meio como solução ótima:

            search_magnitude=(bk+ak)/2
            termination=False

        else:
            k+=1
    return search_magnitude

#═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬

# Definindo o método do gradiente ("steepest descent")

# Definindo um dataframe (pandas.dataframe) para salvar as soluções e direções para cada iteração:

points_checked=pd.DataFrame(columns=["xk","dk","alfa","x(k+1)","gradiente","fun obj"])

# Escolhendo o ponto inicial, tamanho da incerteza e calculando a primeira direção do gradiente:

len_uncert=1e-9
Xk=np.matrix([[100],[100]])
n=int(Xk.shape[0])
dk=-gradf(Xk[0,0],Xk[1,0])
termination=True

while termination:

    # Calculando a magnitude da direção (alfad) usando o método da seção áurea ("golden search"):

    alfad=golden_search(0,100,1e-9,Xk,dk)

    # Calculando o novo ponto:

    Xk1=Xk+alfad*dk

    # Salvando a informação no dataframe:

    points_checked=points_checked.append({"xk":(np.round(Xk[0,0],2),np.round(Xk[1,0],2)),
                                          "dk":(np.round(dk[0,0],2),np.round(dk[1,0],2)),
                                          "alfa":np.round(alfad,3),
                                          "x(k+1)":np.round(Xk1,2),
                                          "gradiente":np.round(gradf(Xk1[0,0],Xk1[1,0]),2),
                                          "fun obj":np.round(fo(Xk1[0,0],Xk1[1,0]),2)}, ignore_index=True)

    # Atualizando a direção na direção do gradiente "steepest":

    dk=-gradf(Xk1[0,0],Xk1[1,0])

    # Checando o critério de término:

    if LA.norm(gradf(Xk1[0,0],Xk1[1,0]))<len_uncert:
        termination=False
        print("A solução ótima é X*=",Xk1[0],Xk1[1], "e a função objetivo no ótimo é = ",np.round(fo(Xk1[0,0],Xk1[1,0]),2)  )

    #Atualizando Xk:

    Xk=Xk1

#═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬

print('Nº de steps que o método Steepest Descent levou para convergir: ', len(points_checked))

print("Nº de avaliações da função objetivo = " + str(count))

print("Tabela com as iterações: ")

print(points_checked)

print("Caminho do método Steepest Descent sobre as curvas de níveis de f:")
contour = contour(f)

y = points_checked['xk'].values.astype(dtype=[('f0', '<f8'), ('f1', '<f8')])      # Essa é uma transformação que será explicada
                                                                                # no arquivo EXTRA.
z = y.view(np.float64).reshape(y.shape + (-1,))

Contour_path = contour_travel(z, f)
```

2º Método de otimização indireto - ***Gradiente Conjugado ("Conjugate Gradient: Fletcher-Reeves")***:

```
# Inicializando o contador externamente

count = 0

# Definindo o contador que entrará junto com a função fun(x), que dirá quantas vezes fun(x) foi avaliada

def incCount():
    global count
    count = count+1

# Definindo a função objetivo fo(x1,x2):

def fo(x1,x2):
    incCount()
    return 2342.1971697181*x1**0.2857 + 8730.1851*(x2/x1)**0.2857 + 62823.6915040394*(1/x2)**0.2857 - 26190.5553

# Definindo o gradiente da função:

def gradf(x1,x2):
    return np.matrix([[669.165731388461*x1**(-0.7143) - 2494.21388307*(x2/x1)**0.2857/x1],
                      [2494.21388307*(x2/x1)**0.2857/x2 - 17948.7286627041*(1/x2)**0.2857/x2]])

# Visualização gráfica da função para certificar que está correta:

from mpl_toolkits.mplot3d import Axes3D
from matplotlib import cm
from matplotlib.ticker import LinearLocator, FormatStrFormatter

fig = plt.figure()
ax = fig.gca(projection='3d')

X = np.linspace(150, 300, 50)
Y = np.linspace(350, 500, 50)
X, Y = np.meshgrid(X, Y)
Z = fo(X,Y)

surf = ax.plot_surface(X, Y, Z,cmap=cm.coolwarm)
ax.set_xlabel('$x1$')
ax.set_ylabel('$x2$')
ax.set_zlabel('$f(x1,x2)$');

plt.show()

#═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬

# Definindo o método do gradiente conjugado ("Fletcher Reeves")

# Definindo um dataframe (pandas.dataframe) para salvar as soluções e direções para cada iteração:

points_checked=pd.DataFrame(columns=["xk","alfacon","dk","alfa","x(k+1)","gradiente","fun obj"])

# Escolhendo o ponto inicial, tamanho da incerteza e calculando a primeira direção do gradiente:

len_uncert=1e-9
Xk=np.matrix([[100],[100]])
n=int(Xk.shape[0])
dk=-gradf(Xk[0,0],Xk[1,0])
termination=True

while termination:

    # Calculando a magnitude da direção (alfad) usando o método da seção áurea ("golden search"):

    alfad=golden_search(0,100,1e-9,Xk,dk)

    # Calculando o novo ponto:

    Xk1=Xk+alfad*dk

    # Calculando "alfacon" para obter uma direção conjugada:

    alfacon=(LA.norm(gradf(Xk1[0,0],Xk1[1,0]))**2)/(LA.norm(gradf(Xk[0,0],Xk[1,0]))**2)

    # Salvando a informação no dataframe:

    points_checked=points_checked.append({"xk":(np.round(Xk[0,0],2),np.round(Xk[1,0],2)),
                                          "alfacon":np.round(alfacon,3),
                                          "dk":(np.round(dk[0,0],2),np.round(dk[1,0],2)),
                                          "alfa":np.round(alfad,3),
                                          "x(k+1)":np.round(Xk1,2),
                                          "gradiente":np.round(gradf(Xk1[0,0],Xk1[1,0]),2),
                                          "fun obj":np.round(fo(Xk1[0,0],Xk1[1,0]),2)}, ignore_index=True)

    # Atualizando a direção gradiente:

    dk=-gradf(Xk1[0,0],Xk1[1,0])+alfacon*dk

    # Checando o critério de término:

    if LA.norm(gradf(Xk1[0,0],Xk1[1,0]))<len_uncert:
        termination=False
        print("A solução ótima é X*=",Xk1[0],Xk1[1], "e a função objetivo no ótimo é = ",np.round(fo(Xk1[0,0],Xk1[1,0]),2)  )

    #Atualizando Xk:

    Xk=Xk1

#═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬

print('Nº de steps que o método Gradiente Conjugado levou para convergir: ', len(points_checked))

print("Nº de avaliações da função objetivo = " + str(count))

print("Tabela com as iterações: ")

print(points_checked)

print("Caminho do método Gradiente Conjugado sobre as curvas de níveis de f:")
contour = contour(f)

y = points_checked['xk'].values.astype(dtype=[('f0', '<f8'), ('f1', '<f8')])

z = y.view(np.float64).reshape(y.shape + (-1,))

Contour_path = contour_travel(z, f)
```

3º Método de otimização indireto - ***BFGS (Broyden-Fletcher-Goldfarb-Shanno)***:

```
# Inicializando o contador externamente

count = 0

# Definindo o contador que entrará junto com a função fun(x), que dirá quantas vezes fun(x) foi avaliada

def incCount():
    global count
    count = count+1

# Definindo a função objetivo fo(x1,x2):

def fo(x1,x2):
    incCount()
    return 2342.1971697181*x1**0.2857 + 8730.1851*(x2/x1)**0.2857 + 62823.6915040394*(1/x2)**0.2857 - 26190.5553

# Definindo o gradiente da função:

def gradf(x1,x2):
    return np.matrix([[669.165731388461*x1**(-0.7143) - 2494.21388307*(x2/x1)**0.2857/x1],
                      [2494.21388307*(x2/x1)**0.2857/x2 - 17948.7286627041*(1/x2)**0.2857/x2]])

# Visualização gráfica da função para certificar que está correta:

from mpl_toolkits.mplot3d import Axes3D
from matplotlib import cm
from matplotlib.ticker import LinearLocator, FormatStrFormatter

fig = plt.figure()
ax = fig.gca(projection='3d')

X = np.linspace(150, 300, 50)
Y = np.linspace(350, 500, 50)
X, Y = np.meshgrid(X, Y)
Z = fo(X,Y)

surf = ax.plot_surface(X, Y, Z,cmap=cm.coolwarm)
ax.set_xlabel('$x1$')
ax.set_ylabel('$x2$')
ax.set_zlabel('$f(x1,x2)$');

plt.show()

#═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬

# Definindo o método do BFGS (Broyden-Fletcher-Goldfarb-Shanno)

# Definindo um dataframe (pandas.dataframe) para salvar as soluções e direções para cada iteração:

points_checked=pd.DataFrame(columns=["xk","Dk","dk","alfa","x(k+1)","gradiente","fun obj"])

# Escolhendo o ponto inicial, tamanho da incerteza e calculando a primeira direção do gradiente:

len_uncert=1e-9
Xk=np.matrix([[100],[100]])
n=int(Xk.shape[0])
Dk=np.eye(n)               #Dk = matriz "H(xk)" [inicialmente matriz identidade]
termination=True

while termination:

    #Calculando as direções usando a matriz Dk

    dk=-Dk*gradf(Xk[0,0],Xk[1,0])

    # Calculando a magnitude da direção (alfad) usando o método da seção áurea ("golden search"):

    alfad=golden_search(0,100,1e-9,Xk,dk)

    # Calculando o novo ponto:

    Xk1=Xk+alfad*dk

    # Calculando Ck usando o método BFGS:                        # Ck são aqueles 2 termos de correção de H(xk)

    pk=Xk1-Xk                                                    # pk = "delta x" (Ver notação do slide 57)

    qk=gradf(Xk1[0,0],Xk1[1,0])-gradf(Xk[0,0],Xk[1,0])           # qk = "delta f"

    Ck=(((pk*pk.T)/(pk.T*qk))*float((1+((qk.T*Dk*qk)/(pk.T*qk)))))-((Dk*qk*pk.T+pk*qk.T*Dk)/(pk.T*qk))

        # Salvando a informação no dataframe:

    points_checked=points_checked.append({"xk":(np.round(Xk[0,0],2),np.round(Xk[1,0],2)),
                                          "Dk":np.round(Dk,3),
                                          "dk":(np.round(dk[0,0],2),np.round(dk[1,0],2)),
                                          "alfa":np.round(alfad,3),
                                          "x(k+1)":np.round(Xk1,2),
                                          "gradiente":np.round(gradf(Xk1[0,0],Xk1[1,0]),2),
                                          "fun obj":np.round(fo(Xk1[0,0],Xk1[1,0]),2)}, ignore_index=True)

    # Atualizando Dk

    Dk=Dk+Ck

    # Checando o critério de término:

    if LA.norm(gradf(Xk1[0,0],Xk1[1,0]))<len_uncert:
        termination=False
        print("A solução ótima é X*=",Xk1[0],Xk1[1], "e a função objetivo no ótimo é = ",np.round(fo(Xk1[0,0],Xk1[1,0]),2)  )

    #Atualizando Xk:

    Xk=Xk1

#═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬

print('Nº de steps que o método BFGS levou para convergir: ', len(points_checked))

print("Nº de avaliações da função objetivo = " + str(count))

print("Tabela com as iterações: ")

print(points_checked)

print("Caminho do método BFGS sobre as curvas de níveis de f:")
contour = contour(f)

y = points_checked['xk'].values.astype(dtype=[('f0', '<f8'), ('f1', '<f8')])

z = y.view(np.float64).reshape(y.shape + (-1,))

Contour_path = contour_travel(z, f)
```

4º Método de otimização indireto - ***DFP (Davidon-Fletcher-Powell)***:

```
# Inicializando o contador externamente

count = 0

# Definindo o contador que entrará junto com a função fun(x), que dirá quantas vezes fun(x) foi avaliada

def incCount():
    global count
    count = count+1

# Definindo a função objetivo fo(x1,x2):

def fo(x1,x2):
    incCount()
    return 2342.1971697181*x1**0.2857 + 8730.1851*(x2/x1)**0.2857 + 62823.6915040394*(1/x2)**0.2857 - 26190.5553

# Definindo o gradiente da função:

def gradf(x1,x2):
    return np.matrix([[669.165731388461*x1**(-0.7143) - 2494.21388307*(x2/x1)**0.2857/x1],
                      [2494.21388307*(x2/x1)**0.2857/x2 - 17948.7286627041*(1/x2)**0.2857/x2]])

# Visualização gráfica da função para certificar que está correta:

from mpl_toolkits.mplot3d import Axes3D
from matplotlib import cm
from matplotlib.ticker import LinearLocator, FormatStrFormatter

fig = plt.figure()
ax = fig.gca(projection='3d')

X = np.linspace(150, 300, 50)
Y = np.linspace(350, 500, 50)
X, Y = np.meshgrid(X, Y)
Z = fo(X,Y)

surf = ax.plot_surface(X, Y, Z,cmap=cm.coolwarm)
ax.set_xlabel('$x1$')
ax.set_ylabel('$x2$')
ax.set_zlabel('$f(x1,x2)$');

plt.show()

#═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬

# Definindo o método do DFP (Davidon-Fletcher-Powell)

# Definindo um dataframe (pandas.dataframe) para salvar as soluções e direções para cada iteração:

points_checked=pd.DataFrame(columns=["xk","Dk","dk","alfa","x(k+1)","gradiente","fun obj"])

# Escolhendo o ponto inicial, tamanho da incerteza e calculando a primeira direção do gradiente:

len_uncert=1e-9
Xk=np.matrix([[100],[100]])
n=int(Xk.shape[0])
Dk=np.eye(n)               #Dk = matriz "W(xk)" [inicialmente matriz identidade]
termination=True

while termination:

    #Calculando as direções usando a matriz Dk

    dk=-Dk*gradf(Xk[0,0],Xk[1,0])

    # Calculando a magnitude da direção (alfad) usando o método da seção áurea ("golden search"):

    alfad=golden_search(0,100,1e-9,Xk,dk)

    # Calculando o novo ponto:

    Xk1=Xk+alfad*dk

    #Calculando Ck usando o método DFP:                              # Ck são aqueles 2 termos de correção de W(xk)

    pk=Xk1-Xk                                                        # pk = delta x (Ver notação do slide 58)
    qk=gradf(Xk1[0,0],Xk1[1,0])-gradf(Xk[0,0],Xk[1,0])               # qk = delta f
    Ck=((pk*pk.T)/(pk.T*qk))-((Dk*qk*qk.T*Dk)/(qk.T*Dk*qk))

        # Salvando a informação no dataframe:

    points_checked=points_checked.append({"xk":(np.round(Xk[0,0],2),np.round(Xk[1,0],2)),
                                          "Dk":np.round(Dk,3),
                                          "dk":(np.round(dk[0,0],2),np.round(dk[1,0],2)),
                                          "alfa":np.round(alfad,3),
                                          "x(k+1)":np.round(Xk1,2),
                                          "gradiente":np.round(gradf(Xk1[0,0],Xk1[1,0]),2),
                                          "fun obj":np.round(fo(Xk1[0,0],Xk1[1,0]),2)}, ignore_index=True)

    # Atualizando Dk

    Dk=Dk+Ck

    # Checando o critério de término:

    if LA.norm(gradf(Xk1[0,0],Xk1[1,0]))<len_uncert:
        termination=False
        print("A solução ótima é X*=",Xk1[0],Xk1[1], "e a função objetivo no ótimo é = ",np.round(fo(Xk1[0,0],Xk1[1,0]),2)  )

    #Atualizando Xk:

    Xk=Xk1

#═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬═════════╬

print('Nº de steps que o método DFP levou para convergir: ', len(points_checked))

print("Nº de avaliações da função objetivo = " + str(count))

print("Tabela com as iterações: ")

print(points_checked)

print("Caminho do método DFP sobre as curvas de níveis de f:")
contour = contour(f)

y = points_checked['xk'].values.astype(dtype=[('f0', '<f8'), ('f1', '<f8')])

z = y.view(np.float64).reshape(y.shape + (-1,))

Contour_path = contour_travel(z, f)
```

***ADICIONAL:*** Método de otimização indireto - ***BFGS (Broyden-Fletcher-Goldfarb-Shanno)*** por meio do Scipy.optimize.minimize:

```
import numpy as np
from scipy.optimize import minimize

x0 = np.array([100, 100])

# Definindo a função objetivo f(x):

def f(x):
    return 2342.1971697181*x[0]**0.2857 + 8730.1851*(x[1]/x[0])**0.2857 + 62823.6915040394*(1/x[1])**0.2857 - 26190.5553

# Definindo o gradiente da função:

def gradf(x):
    return np.array([669.165731388461*x[0]**(-0.7143) - 2494.21388307*(x[1]/x[0])**0.2857/x[0],
                      2494.21388307*(x[1]/x[0])**0.2857/x[1] - 17948.7286627041*(1/x[1])**0.2857/x[1]])

res = minimize(f, x0, method='BFGS', jac=gradf, options={'gtol': 1e-9, 'disp': True})

res.x
```

    Optimization terminated successfully.
             Current function value: 6421.490224
             Iterations: 18
             Function evaluations: 21
             Gradient evaluations: 21

    array([215.443469  , 464.15888336])

***Resposta: (b)*** Podemos fazer comparações entre os métodos, de acordo com a seguinte tabela:

Como podemos verificar, praticamente todos os métodos encontraram o mesmo ponto ótimo, com a mesmo valor da função objetivo. O único método que tem uma ligeira alteração no ponto ótimo é o de Rosenbrock (~0,01 em p2, p3).

A convergência dos métodos foi parecida, porque a função objetivo no caso é bem definida, diferenciável em todo o intervalo de x, e convexa. Se as derivadas de f(x) não estivessem disponíveis, não poderíamos usar os métodos indiretos (em laranja).

A maior diferença que podemos detectar para este caso, é a tendência de um maior número de iterações/avaliações da função objetivo para os métodos indiretos; especialmente para os métodos do DFP e BFGS implementados.

$\\
$

$\\
$
**9)**  Resolva novamente os exercícios (4b) e (4d) utilizando o método SQP e
levando-se em conta as restrições do problema e $L(x) \geq 0.$

$\\
$

## ***Solução:***

Quando resolvemos os problemas $4(b)$ e $4(d)$, utilizamos o método de otimização ***SLSQP***. Na realidade, o SLSQP é análogo ao ***SQP***.

Mais precisamente, SQP e SLSQP apresentam o mesmo subproblema da Programação Quadrática (***QP***) em cada etapa do algoritmo.

No SQP, o problema de QP é resolvido por métodos de Programação Quadrática.

Em SLSQP, para resolver o problema de QP, você deve fatorar em $LDL^{-1}$ o lagrangeano da Hessiana e então resolver um problema de mínimos quadrados lineares:

>$\underset{d\in \mathbb{R}^{n}}{min}\left \| \left ( D^{k} \right )^{1/2}\left ( L^{k} \right )^{T}d + \left ( D^{k} \right )^{-1/2}\left ( L^{k} \right )^{-1}\triangledown f(x^{k})\right \|\\
\triangledown g_{j}(x^{k})d+g_{j}(x^{k})=0, \quad\ \quad\ j=1,...,m_{c}, \\
\triangledown g_{j}(x^{k})d+g_{j}(x^{k})\geq 0, \quad\ \quad\ j=m_{c}+1, ..., m $

Como os dois métodos resolvem o mesmo problema QP, vamos continuar considerando o SLSQP aqui.

E, retornando aos problemas $4(b)$ e $4(d)$, já havíamos considerado as restrições dadas pelo problema original.

A única restrição que está faltando então é $L(x) \geq 0:$

```
#Retornando ao problema 4(b):

import numpy as np
import scipy.integrate
import matplotlib.pyplot as plt
%matplotlib inline
from scipy.optimize import minimize

#Verificando a forma da superfície:

from matplotlib import cm

x1 = np.linspace(1E-3, 2E-2, 50)
x2 = np.linspace(1E-3, 2E-2, 50)
X, Y = np.meshgrid(x1, x2)

Z = 129.93 - 0.5/X - 4000*Y - 25*(X/Y)

fig, ax = plt.subplots(subplot_kw={'projection': '3d'})
ax.plot_surface(X, Y, Z, cmap=cm.rainbow)

ax.set_xlabel('$x1$')
ax.set_ylabel('$x2$')
ax.set_zlabel('$L(x1,x2)$');
```

```
def func(x, a=129.93, b=0.5, c=4000, d=25, sign=-1.0):
    return sign*(a - b/x[0] - c*x[1] - d*x[0]/x[1])

def func_deriv(x, a=129.93, b=0.5, c=4000, d=25, sign=-1.0):
    dfdx0 = sign*(0 + b/(x[0])**2 + 0 - d/x[1])
    dfdx1 = sign*(0 + 0 - c + d*x[0]/(x[1])**2)
    return np.array([ dfdx0, dfdx1 ])

from scipy.optimize import Bounds
bounds = Bounds([1E-9, 1E-9], [1.0, 1.0])

#Agora, além das restrições de desigualdade x1 <= 0,02, x2 <= x1;
#temos que adicionar também a desigualdade func(x) >= 0:

ineq_cons = {'type': 'ineq',
             'fun' : lambda x: np.array([0.02 - x[0],
                                         x[0] - x[1],
                                         func(x, sign=1.0)]),
             'jac' : lambda x: np.array([[-1.0, 0.0],
                                         [1.0, -1.0],
                                         func_deriv(x, sign=1.0)])}
```

```
#Executando a otimização com restrições (constraints = ineq_cons):

x0 = np.array([0.5, 0.5])
res = minimize(func, x0, jac=func_deriv, constraints=ineq_cons,
               method='SLSQP', options={'ftol': 1e-9, 'disp': True},
               bounds = bounds)
print(res.x)
```

    Optimization terminated successfully.    (Exit mode 0)
                Current function value: -19.409055040739943
                Iterations: 14
                Function evaluations: 21
                Gradient evaluations: 14
    [0.01357208 0.00921008]

```
#Resultado em notação científica:

scientific_notation1 = "{:.2e}".format(res.x[0])

scientific_notation2 = "{:.2e}".format(res.x[1])

print(scientific_notation1 + "," + scientific_notation2)
```

    1.36e-02,9.21e-03

```
#Valor da função objetivo (lucro) no ponto máximo:

#Lembrar de alterar o sinal novamente para sign=1.0:

def func(x, a=129.93, b=0.5, c=4000, d=25, sign=1.0):
    return sign*(a - b/x[0] - c*x[1] - d*x[0]/x[1])

result = func([1.36E-2, 9.21E-3])

print(result)
```

    19.40889889506292

```
#Retornando ao problema 4(d):

import numpy as np
import scipy.integrate
import matplotlib.pyplot as plt
%matplotlib inline
from scipy.optimize import minimize

#Verificando a forma da superfície:

from matplotlib import cm

x1 = np.linspace(1E-2, 5E-2, 50)
x2 = np.linspace(1E-2, 1E-1, 50)
X, Y = np.meshgrid(x1, x2)

Z = 279.72 - 2.0/X - 4000*Y - 100*(X/Y)

fig, ax = plt.subplots(subplot_kw={'projection': '3d'})
ax.plot_surface(X, Y, Z, cmap=cm.rainbow)

ax.set_xlabel('$x1$')
ax.set_ylabel('$x2$')
ax.set_zlabel('$L(x1,x2)$');
```

```
def func(x, a=279.72, b=2.0, c=4000, d=100, sign=-1.0):
    return sign*(a - b/x[0] - c*x[1] - d*x[0]/x[1])

def func_deriv(x, a=279.72, b=2.0, c=4000, d=100, sign=-1.0):
    dfdx0 = sign*(0 + b/(x[0])**2 + 0 - d/x[1])
    dfdx1 = sign*(0 + 0 - c + d*x[0]/(x[1])**2)
    return np.array([ dfdx0, dfdx1 ])

from scipy.optimize import Bounds
bounds = Bounds([1E-9, 1E-9], [1.0, 1.0])

#Agora, além das restrições de desigualdade x1 <= 0,02, x2 <= x1;
#temos que adicionar também a desigualdade func(x) >= 0:

ineq_cons = {'type': 'ineq',
             'fun' : lambda x: np.array([0.02 - x[0],
                                         x[0] - x[1],
                                         func(x, sign=1.0)]),
             'jac' : lambda x: np.array([[-1.0, 0.0],
                                         [1.0, -1.0],
                                         func_deriv(x, sign=1.0)])}
```

```
#E a otimização com restrições (OPCIONAL - constraints = ineq_cons):

x0 = np.array([0.5, 0.5])
res = minimize(func, x0, jac=func_deriv, constraints=ineq_cons,
               method='SLSQP', options={'ftol': 1e-9, 'disp': True},
               bounds = bounds)
print(res.x)
```

    Inequality constraints incompatible    (Exit mode 4)
                Current function value: 0.2642368615241253
                Iterations: 11
                Function evaluations: 33
                Gradient evaluations: 10
    [0.02001582 0.02001583]

```
#Valor da função objetivo (lucro) no ponto máximo (com restrições):

#Lembrar de alterar o sinal novamente para sign=1.0:

def func(x, a=279.72, b=2.0, c=4000, d=100, sign=1.0):
    return sign*(a - b/x[0] - c*x[1] - d*x[0]/x[1])

result = func([0.02001582, 0.02001583])

print(result)
```

    -0.26423255819140934

***Resposta: (9)***  A nova restrição $L(x) \geq 0$ efetivamente não teve nenhum impacto sobre a solução do problema ***4(b)***. O novo ponto ótimo e o valor da função objetivo (Lucro) permaneceram inalterados:

>$x = (1,36 \cdot 10^{-2}, 9,21 \cdot 10^{-3})$
>
>$L(x) = 19,41$

Obviamente, não esperava-se um resultado diferente, visto que a solução do problema 4(b) já satisfazia a nova restrição imposta $L(x) \geq 0$.

$\\
$

Já para a solução do problema ***4(d)***, essa nova restrição proporcionou uma leve alteração no resultado, praticamente imperceptível:

>$x = (2,002 \cdot 10^{-2}, 2,002 \cdot 10^{-2})$
>
>$L(x) = -0,26$

A solução que tínhamos obtido no passado, no problema 4(d), era:

>$x = (2,00 \cdot 10^{-2}, 2,00 \cdot 10^{-2})$
>
>$L(x) = -0,28$

Esse não era o resultado que se esperava obter, já que essa nova condição imposta gerou uma solução que não satisfaz 2 restrições: $x_{1} \leq 0,$ e a própria restrição $L(x) \geq 0$.

Sendo mais criterioso, ela também viola a terceira condição: $x_{2}  \leq x_{1}, $ na última casa decimal: $x = [0.02001582, 0.02001583].$

Ou seja, essa nova restrição fez com que todas as outras também fossem violadas.

Esse é um resultado estranho, pois esperava-se simplesmente que o algoritmo não convergisse, já que não existe nenhuma solução possível que atenda às 3 restrições impostas. No entanto, ele convergiu para uma solução que viola as 3 condições ao mesmo tempo.

O algoritmo até indica que reconheceu problemas nesse caso, com a mensagem no início de seu output:

>*Inequality constraints incompatible    (Exit mode 4)*

Mas mesmo assim, ele ainda fornece uma resposta final. Provavelmente porque deve existir alguma função dentro do próprio algoritmo do "minimize" que converge para a solução mais próxima desconsiderando-se essa última restrição imposta.

$\\
$


```python
from sympy import *
from barfoot_utils_np import create_rot
init_printing()
```

```python
# State
x, y, z, rho, phi, theta = symbols('x y z \\rho \\phi \\theta', real=True)
xn, yn, zn, rhon, phin, thetan = symbols('xn yn zn rhon phin thetan', real=True)

# Landmark
mx, my, mz = symbols('mx my mz', real=True)

# Control input
vx, vy, vz, wx, wy, wz = symbols('v_x v_y v_z \\omega_x \\omega_y \\omega_z', real=True)
dt = Symbol('dt', real=True)

# Vehicle state and landmark
X = Matrix([x,y,z, rho, phi, theta])
Xn = Matrix([xn,yn,zn, rhon, phin, thetan])
V = Matrix([vx,vy,vz, wx, wy, wz])
m = Matrix([mx, my, mz])
```

```python
def create_rot_sym(Xrot):

    '''
    :param Xrot: Euler angles (1x3)
    '''

    # each dimension
    Rx = rot_axis1(Xrot[0])
    Ry = rot_axis2(Xrot[1])
    Rz = rot_axis3(Xrot[2])

    # Sympy doesn't follow the right-hand rule
    Rz[0,1] = -Rz[0,1]
    Rz[1,0] = -Rz[1,0]
    Ry[0,2] = -Ry[0,2]
    Ry[2,0] = -Ry[2,0]
    Rx[1,2] = -Rx[1,2]
    Rx[2,1] = -Rx[2,1]

    Rxyz = Rz.multiply(Ry).multiply(Rx)

    return Rxyz
```

```python
def dynamics(X, V):

    '''
    :param X: latent state (1x6)
    :param V: control input (1x6)
    '''

    dV = Matrix([V[3:6]])
    Rxyz = create_rot_sym(X[3:6])
    dX = Rxyz.subs([
        (rho, X[3]),
        (phi, X[4]),
        (theta, X[5])
    ]).multiply(Matrix(V[0:3]))
    return Matrix([*dX, *dV])

dynamics(X, V)
```

$\displaystyle \left[\begin{matrix}v_{x} \cos{\left(\phi \right)} \cos{\left(\theta \right)} + v_{y} \left(\sin{\left(\phi \right)} \sin{\left(\rho \right)} \cos{\left(\theta \right)} - \sin{\left(\theta \right)} \cos{\left(\rho \right)}\right) + v_{z} \left(\sin{\left(\phi \right)} \cos{\left(\rho \right)} \cos{\left(\theta \right)} + \sin{\left(\rho \right)} \sin{\left(\theta \right)}\right)\\v_{x} \sin{\left(\theta \right)} \cos{\left(\phi \right)} + v_{y} \left(\sin{\left(\phi \right)} \sin{\left(\rho \right)} \sin{\left(\theta \right)} + \cos{\left(\rho \right)} \cos{\left(\theta \right)}\right) + v_{z} \left(\sin{\left(\phi \right)} \sin{\left(\theta \right)} \cos{\left(\rho \right)} - \sin{\left(\rho \right)} \cos{\left(\theta \right)}\right)\\- v_{x} \sin{\left(\phi \right)} + v_{y} \sin{\left(\rho \right)} \cos{\left(\phi \right)} + v_{z} \cos{\left(\phi \right)} \cos{\left(\rho \right)}\\\omega_x\\\omega_y\\\omega_z\end{matrix}\right]$

```python
def measurment_model(X, L):

    '''
    :param X: latent state (1x6)
    :param L: landmarks
    '''

    T = eye(4)
    T[0:3, 0:3] = create_rot_sym(X[3:6])
    T[0:3, 3] = X[0:3]
    Tinv = eye(4)
    Tinv[0:3, 0:3] = T[0:3, 0:3].T
    Tinv[0:3, 3] = -T_inv[0:3,0:3]*T[0:3,3]

    return Tinv

measurment_model(X, None)
```

$\displaystyle \left[\begin{matrix}\cos{\left(\phi \right)} \cos{\left(\theta \right)} & \sin{\left(\theta \right)} \cos{\left(\phi \right)} & - \sin{\left(\phi \right)} & - x\\\sin{\left(\phi \right)} \sin{\left(\rho \right)} \cos{\left(\theta \right)} - \sin{\left(\theta \right)} \cos{\left(\rho \right)} & \sin{\left(\phi \right)} \sin{\left(\rho \right)} \sin{\left(\theta \right)} + \cos{\left(\rho \right)} \cos{\left(\theta \right)} & \sin{\left(\rho \right)} \cos{\left(\phi \right)} & - y\\\sin{\left(\phi \right)} \cos{\left(\rho \right)} \cos{\left(\theta \right)} + \sin{\left(\rho \right)} \sin{\left(\theta \right)} & \sin{\left(\phi \right)} \sin{\left(\theta \right)} \cos{\left(\rho \right)} - \sin{\left(\rho \right)} \cos{\left(\theta \right)} & \cos{\left(\phi \right)} \cos{\left(\rho \right)} & - z\\0 & 0 & 0 & 1\end{matrix}\right]$


```python
from __future__ import division
import numpy as np
import matplotlib.pyplot as plt
import scipy
from scipy import optimize
import sympy as sm
sm.init_printing(use_unicode=True)
```

# 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.0
beta = 0.96
gamma = 0.1
w = 2.0
b = 1.0
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).

```python
c2 = sm.symbols("c_2")
l2 = sm.symbols("l_2")
c1 = sm.symbols("c_1")
l1 = sm.symbols("l_1")
h2 = sm.symbols("h_2")
h1 = sm.symbols("h_1")
l = sm.symbols("l")
rho_2 = sm.symbols("rho")
gamma_2 = sm.symbols("gamma")
w_2 = sm.symbols("w")
```

```python
#Defining relevant equations
def util(h, l):
    return consumption(h, l)**(1-rho)/(1-rho)-gamma*l

def consumption(h, l):
    if l == 1:
        return w*h
    elif l == 0:
        return b

def v2util(h, l):
    return util(h, l)

#Solve cutoff value between work and not working
cutoff_p2 = sm.solve(v2util(h2, 1)-v2util(h2, 0), h2)
print(f'Cutoff value is: {cutoff_p2}. The individual works if their level of human capital is above this value as utility is increasing in human capital.')
```

    Cutoff value is: [0.555555555555556]. The individual works if their level of human capital is above this value as utility is increasing in human capital.

```python
#Plotting the cutoff
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
ax.plot(h_vec, v2util(h_vec, 1), "r-", label='Working')
ax.axhline(y=v2util(h_vec, 0), xmin=0, xmax=2, label='Not working')
plt.title("Utility as a function of human capital")
plt.xlabel('Human capital')
plt.ylabel('Utility')
plt.legend()
plt.show()
```

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

```python
#Defining the relevant equations
def utilp1(h, l):
    return consumptionp1(h, l)**(1-rho)/(1-rho)-gamma*l+beta*v2util(human_cap(h, l), l)

def human_cap(h, l):
    return h + l + 0.5*delta

def consumptionp1(h, l):
    if l == 1:
        return w*h
    elif l == 0:
        return b
#Calculating the cutoff value
cutoff_p1 = sm.solve(utilp1(h1, 1)-utilp1(h1, 0), h1)
print(f'The solution for period 1 is: {cutoff_p1}. As the relevant levels of human capital are positive, we are left with the solution: {cutoff_p1[1]:.2f}')
```

    The solution for period 1 is: [-0.846169527733768, 0.351725083289324]. As the relevant levels of human capital are positive, we are left with the solution: 0.35

```python
#Plotting the cutoff
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
ax.plot(h_vec, utilp1(h_vec, 1), "r-", label='Working')
ax.axhline(y=utilp1(h_vec, 0), xmin=0, xmax=2, label='Not working')
plt.title("Utility as a function of human capital in period 1")
plt.xlabel('Human capital')
plt.ylabel('Utility')
plt.legend()
plt.show()
```

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

```python
#Let's try setting b > w
b = 2.5
#Plotting the cutoff
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
ax.plot(h_vec, utilp1(h_vec, 1), "r-", label='Working')
ax.axhline(y=utilp1(h_vec, 0), xmin=0, xmax=2, label='Not working')
plt.title("Utility as a function of human capital in period 1")
plt.xlabel('Human capital')
plt.ylabel('Utility')
plt.legend()
plt.show()
```

As we see, the individual can choose to work in period 1, if the level of human capital is large enough. The cutoff is within the 'relevant' level of acquired human capital. Therefore the worker may choose to work, even though the unemployment benefits are larger than the wage.

# 2. AS-AD model

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

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

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

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

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

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

The **ex ante real interest rate** is given by

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

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

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

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

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

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

**Inflation expectations are adaptive** and given by

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

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

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

The **parameters** of the model are:

```python
par = {}

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

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

```python
pt = sm.symbols("pi_t")
ptminus = sm.symbols("pi_t-1")
yt = sm.symbols("y_t")
ytminus = sm.symbols("y_t-1")
st = sm.symbols("s_t")
stminus = sm.symbols("s_t-1")
vt = sm.symbols("v_t")
alpha = sm.symbols("alpha")
gamma = sm.symbols("gamma")
phi = sm.symbols("phi")
h = sm.symbols("h")
b = sm.symbols("b")
SRAS = sm.Eq(pt,ptminus+gamma*yt-phi*gamma*ytminus+st-phi*stminus)
SRAS

```

```python
AD = sm.Eq(pt,(1/(h*alpha)*(vt-(1+b*alpha)*yt)))
AD
```

```python
#Came back to define the function as it was needed below.
def SRASandAD(h, alpha, b, phi, gamma):
    return sm.Eq((1/(h*alpha)*(vt-(1+b*alpha)*yt)),ptminus+gamma*yt-phi*gamma*ytminus+st-phi*stminus)
SRASandAD(h, alpha, b, phi, gamma)
```

```python
#solution for y
solutiony21 = sm.solve(SRASandAD(h, alpha, b, phi, gamma), yt)
#came back to store this for 2.3
yt21 = sm.lambdify((h, alpha, vt, b, st, phi, stminus, ptminus, ytminus, gamma), solutiony21[0])
solutiony21
```

```python
#Solution for pi
solutionpi21 = (1/(h*alpha)*(vt-(1+b*alpha)*solutiony21[0]))
#came back to store this for 2.3
pi21 = sm.lambdify((h, alpha, vt, b, st, phi, stminus, ptminus, ytminus, gamma), solutionpi21)
solutionpi21
```

Solving with parameter values:

```python
solutiony21_par = sm.solve(SRASandAD(par['h'], par['alpha'], par['b'], par['phi'], par['gamma']), yt)
solutiony21_par
```

```python
solutionpi21_par = (1/(par['h']*par['alpha'])*(vt-(1+par['b']*par['alpha'])*solutiony21_par[0]))
solutionpi21_par
```

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

```python
ptminus = 0
ytminus = 0
vt = 0
st = 0
stminus = 0
#Finding solution for y_t
solutiony22_par = sm.solve(SRASandAD(par['h'], par['alpha'], par['b'], par['phi'], par['gamma']), yt)
solutiony22_par
```

```python
#solution for pi_t
solutionpi22_par = (1/(par['h']*par['alpha'])*(vt-(1+par['b']*par['alpha'])*solutiony22_par[0]))
solutionpi22_par
```

```python
#now vt = 0.1
#Solution for y
vt = 0.1
solutiony22_vt = sm.solve(SRASandAD(par['h'], par['alpha'], par['b'], par['phi'], par['gamma']), yt)
solutiony22_vt
```

```python
#Solution for PI
solutionpi22_vt = (1/(par['h']*par['alpha'])*(vt-(1+par['b']*par['alpha'])*solutiony22_vt[0]))
solutionpi22_vt
```

```python
#plotting the AS-AD model
xs2 = np.linspace(-1,1)
novtsolasad = lambda x: (1.0/(par['h']*par['alpha'])*(-(1.0+par['b']*par['alpha'])*x)) #no vt
vtsolasad = lambda x: (1.0/(par['h']*par['alpha'])*(0.1-(1.0+par['b']*par['alpha'])*x)) #vt
srassol = lambda x: par['gamma']*x #sras

novtsol = novtsolasad(xs2)
vtsol = vtsolasad(xs2)
sras = srassol(xs2)

plt.plot(xs2, vtsol, 'b-', label='$V_t = 0.1$')
plt.plot(xs2, novtsol, 'r-', label = "$V_t = 0$")
plt.plot(xs2, sras, 'g-', label= "SRAS")
plt.xlabel('$y_t$')
plt.ylabel('inflation')
plt.axis([-0.05, 0.05, -0.1, 0.1])
plt.title("AS-AD model")
plt.legend()
plt.show()
```

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

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

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

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

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

```python
#Let's define the processes
def vt(vtminus, x):
    return par['delta']*vtminus + x
def st(stminus, c):
    return par['omega']*stminus + c

T = 200
x = np.zeros(T)
c = np.zeros(T)
yminus = [0]
piminus = [0]
sminus = [0]
vminus = [0]
x[1] = 0.1

#using the stored solutions from 2.1 to simulate the process

for t in range(1, T):
    vminus.append(vt(vminus[t-1], x[t]))
    sminus.append(st(sminus[t-1], c[t]))
    yminus.append(yt21(par['h'], par['alpha'], vminus[t], par['b'], sminus[t], par['phi'], sminus[t-1], piminus[t-1], yminus[t-1], par['gamma']))
    piminus.append(pi21(par['h'], par['alpha'], vminus[t], par['b'], sminus[t], par['phi'], sminus[t-1], piminus[t-1], yminus[t-1], par['gamma']))

fig, (ax1, ax2) = plt.subplots(1, 2)
ax1.plot(yminus, label = "Output gap")
ax1.set_title("Output over time")
ax2.plot(piminus, label = "Inflation gap")
ax2.set_title("Inflation over time")
ax1.set_xlabel("Time")
ax2.set_xlabel("Time")
ax1.set_ylabel("Output")
ax2.set_ylabel("Inflation")
fig.set_figwidth(20)
plt.show()

```

**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
#Same approach as in 2.3. New distribution, though. I should have made a function in hindsight
#Function is defined in the next cell.

T = 1000
np.random.seed(6969)
x = np.random.normal(loc = 0, scale = par['sigma_x'], size = T)
c = np.random.normal(loc = 0, scale = par['sigma_c'], size = T)
yminus = [0]
piminus = [0]
sminus = [0]
vminus = [0]
x[1] = 0.1

for t in range(1, T):
    vminus.append(vt(vminus[t-1], x[t]))
    sminus.append(st(sminus[t-1], c[t]))
    yminus.append(yt21(par['h'], par['alpha'], vminus[t], par['b'], sminus[t], par['phi'], sminus[t-1], piminus[t-1], yminus[t-1], par['gamma']))
    piminus.append(pi21(par['h'], par['alpha'], vminus[t], par['b'], sminus[t], par['phi'], sminus[t-1], piminus[t-1], yminus[t-1], par['gamma']))

fig, (ax1, ax2) = plt.subplots(1, 2)
ax1.plot(yminus, label = "Output gap")
ax1.set_title("Output over time")
ax2.plot(piminus, label = "Inflation gap")
ax2.set_title("Inflation over time")
ax1.set_xlabel("Time")
ax2.set_xlabel("Time")
ax1.set_ylabel("Output")
ax2.set_ylabel("Inflation")
fig.set_figwidth(20)
plt.show()

```

```python
print(f'The variance is (y, pi) = {np.var(yminus), np.var(piminus)}')
```

    The variance is (y, pi) = (1.8145391852221522, 1.0238862692255288)

```python
print(f'The correlation coefficient between y and pi is: {np.corrcoef(yminus, piminus)}')
```

    The correlation coefficient between y and pi is: [[ 1.         -0.13784279]
     [-0.13784279  1.        ]]

```python
print(f'The autocorrelation between yt and y_(t-1) is: {np.corrcoef(np.array([yminus[:-1], yminus[1:]]))}')
```

    The autocorrelation between yt and y_(t-1) is: [[1.         0.76075706]
     [0.76075706 1.        ]]

```python
print(f'The autocorrelation between pi_t and pi_(t-1) is: {np.corrcoef(np.array([piminus[:-1], piminus[1:]]))}')
```

    The autocorrelation between pi_t and pi_(t-1) is: [[1.         0.97795762]
     [0.97795762 1.        ]]

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

```python
def rootfinder_phi(phi):
    T = 1000
    np.random.seed(6969)
    x = np.random.normal(loc = 0, scale = par['sigma_x'], size = T)
    c = np.random.normal(loc = 0, scale = par['sigma_c'], size = T)
    yminus = [0]
    piminus = [0]
    sminus = [0]
    vminus = [0]
    x[1] = 0.1
    #Adding the correlation
    corr_y_pi = [0]
    for t in range(1, T):
        vminus.append(vt(vminus[t-1], x[t]))
        sminus.append(st(sminus[t-1], c[t]))
        yminus.append(yt21(par['h'], par['alpha'], vminus[t], par['b'], sminus[t], phi, sminus[t-1], piminus[t-1], yminus[t-1], par['gamma']))
        piminus.append(pi21(par['h'], par['alpha'], vminus[t], par['b'], sminus[t], phi, sminus[t-1], piminus[t-1], yminus[t-1], par['gamma']))
        corr_y_pi = np.corrcoef(yminus, piminus)[1, 0]
    return yminus, piminus, corr_y_pi

```

```python
opti_fun = lambda phi: np.corrcoef(rootfinder_phi(phi)[0], rootfinder_phi(phi)[1])[1, 0]-0.31

opti_fun_root = optimize.brentq(opti_fun, a = 0, b = 1, full_output = False)
print(f'The value of phi, such that the correlation coefficient is closest to 0.31 is: {opti_fun_root}')
```

    The value of phi, such that the correlation coefficient is closest to 0.31 is: 0.9562298823595026

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

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

```python
#Getting an error and cannot think of a way to pass it. My approach would have been extending the method above to fit in multiple criteria.

#def USbusiness(phi, c, x):
#    T = 1000
#    np.random.seed(6969)
#    x = np.random.normal(loc = 0, scale = par['sigma_x'], size = T)
#    c = np.random.normal(loc = 0, scale = par['sigma_c'], size = T)
#    yminus = [0]
#    piminus = [0]
#    sminus = [0]
#    vminus = [0]
#    x[1] = 0.1
#    corr_y_pi = [0]
#    corr_y_y = [0]
#    corr_pi_pi = [0]
#    var_y = [0]
#    var_pi = [0]

#    for t in range(1, T):
#        vminus.append(vt(vminus[t-1], x[t]))
#        sminus.append(st(sminus[t-1], c[t]))
#        yminus.append(yt21(par['h'], par['alpha'], vminus[t], par['b'], sminus[t], phi, sminus[t-1], piminus[t-1], yminus[t-1], par['gamma']))
#        piminus.append(pi21(par['h'], par['alpha'], vminus[t], par['b'], sminus[t], phi, sminus[t-1], piminus[t-1], yminus[t-1], par['gamma']))
#        corr_y_pi = np.corrcoef(yminus, piminus)[1, 0]
#        corr_y_y = np.corrcoef(yminus[1:], yminus[:-1])[1, 0]
#        corr_pi_pi = np.corrcoef(piminus[1:], piminus[:-1])[1, 0]
#        var_y = np.var(yminus)
#        var_pi = np.var(piminus)
#        return var_pi, var_y, yminus, piminus, corr_y_y, corr_pi_pi, corr_y_pi

#var_pi, var_y, yminus, piminus, corr_y_y, corr_pi_pi, corr_y_pi   = USbusiness(pi, c, x)

```

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

#Let's check how beta looks for the upcoming task
betas
```

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

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

```python
fig, ax = plt.subplots(1, 3)
ax[0].hist(betas[:,0], bins = 100, density = 'True')
ax[0].set_title("Good 1")
ax[1].hist(betas[:,1], bins = 100, density = 'True')
ax[1].set_title("Good 2")
ax[2].hist(betas[:,2], bins = 100, density = 'True')
ax[2].set_title("Good 3")
fig.set_figwidth(20)
plt.show()
```

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.

```python
#Demand for each good

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

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

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

#excess demand

def excess_demand(x, p1, p2, e1, e2, e3, betas):
    demand = np.sum(x(p1, p2, e1, e2, e3, betas))

    if x == "x1":
        supply = np.sum(e1)
    elif x == "x2":
        supply = np.sum(e2)
    else:
        supply = np.sum(e3)
    exc_dem = demand - supply
    return exc_dem

#price_1 = np.linspace(0.1, 10, 100)
#price_2 = np.linspace(0.1, 10, 100)
#price_3 = np.linspace(0.1, 10, 100)

#x1_fun = lambda x: excess_demand(x1, x, p2, e1, e2, e3, betas)
#x1_fun_plot = x1_fun(price_1)
#x2_fun = lambda x: excess_demand(x2, p1, x, e1, e2, e3, betas)
#x2_fun_plot = x2_fun(price_2)

#plt.plot(price_1, x1_fun_plot, "r-", label = "Product 1")
#plt.plot(price_2, x2_fun_plot, "b-", label = "Product 2" )
#plt.show()

#Since the plot requires three axes it got quite messy and with errors all over the place. I ended up deleting it and not being able to recreate anything meaningful.
```

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

```python
#Defining the criteria written above

p1_ini = 1
p2_ini = 1
#I had to set a cap on iterations or the loop will spin forever in q5
def equilibrium(iteration, p1, p2, e1, e2, e3, betas, kappa, epsilon):
    t=0
    while True:
        z1 = excess_demand(x1, p1, p2, e1, e2, e3, betas)
        z2 = excess_demand(x2, p1, p2, e1, e2, e3, betas)
        if np.abs(z1) < epsilon and np.abs(z2) < epsilon or t > iteration: #Needed to avoid waiting for minutes.
            print(f'The prices are (p1, p2) = {p1, p2}, and the excess demand is (z1, z2) = {z1, z2}. We keep in mind that p3 = 1')
            break
        p1 = p1 + kappa*z1/N
        p2 = p2 + kappa*z2/N
        t += 1
    return p1, p2
p1, p2 = equilibrium(10000, p1_ini, p2_ini, e1, e2, e3, betas, 2, 1e-10)

```

    The prices are (p1, p2) = (6.0596117842277755, 2.433061390839292), and the excess demand is (z1, z2) = (9.458744898438454e-11, 3.637978807091713e-11). We keep in mind that p3 = 1

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

```python
def utility(x1, x2, x3, betas, gamma):
    return (x1**betas[:,0] + x2**betas[:,1] + x3**betas[:,2])**gamma

#using demand functions from above
utility_wal = utility(x1(p1, p2, e1, e2, e3, betas), x2(p1, p2, e1, e2, e3, betas), x3(p1, p2, e1, e2, e3, betas), betas, gamma)
print(f'The mean of the utility is: {utility_wal.mean()}')
```

    The mean of the utility is: 2.3769975524551423

```python
print(f'The variance of the utility is: {utility_wal.var()}')
```

    The variance of the utility is: 0.20702487229161537

```python
#plotting the distribution
fig = plt.figure()
ax = fig.add_subplot(1,1,1)
ax.hist(utility_wal, bins = 100, density = True)
ax.set_title("Distribution of utility of the walras equilibrium")
plt.show()
```

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

```python
#Defining new shares of endowments
new_e = (e1+e2+e3)/3
p1_5, p2_5 = equilibrium(10000, p1_ini, p2_ini, new_e, new_e, new_e, betas, 2, 1e-10)

utility_wal_5  = utility(x1(p1_5, p2_5, new_e, new_e, new_e, betas), x2(p1_5, p2_5, new_e, new_e, new_e, betas), x3(p1_5, p2_5, new_e, new_e, new_e, betas), betas, gamma)
utility_wal_5_dif_gam  = utility(x1(p1_5, p2_5, new_e, new_e, new_e, betas), x2(p1_5, p2_5, new_e, new_e, new_e, betas), x3(p1_5, p2_5, new_e, new_e, new_e, betas), betas, 0.5)

```

    The prices are (p1, p2) = (6.441793456362303, 2.5905935972737115), and the excess demand is (z1, z2) = (9.458744898438454e-11, 4.3655745685100555e-11). We keep in mind that p3 = 1

```python
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
ax.hist(utility_wal_5, bins = 100, density = True)
ax.set_title("Distribution of utility of the walras equilibrium with $gamma = 0.8$")
plt.show()
```

```python
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
ax.hist(utility_wal_5_dif_gam, bins = 100, density = True)
ax.set_title("Distribution of utility of the walras equilibrium with $gamma = 0.5$")
plt.show()
```

We see that the average utility lowers with a decreasing gamma. If we compare it to the previous one in 3.4, we see that the variance is a lot smaller but that the mean stays roughly the same ith an unchanged gamma. A rough conclusion could therefore be, that the distribution of endowments have an insignificant effect on the utility in an exchange economy.

```python

```