# Solutions

## Question 1

> `1`. Using a `for` loop print the types of the variables in each of the
> following iterables:

>  `1`. `iterable = (1, 2, 3, 4)`

```python
for variable in (1, 2, 3, 4):
    print(type(variable))
```

    <class 'int'>
    <class 'int'>
    <class 'int'>
    <class 'int'>

> `2`. `iterable = (1, 2.0, 3, 4.0)`

```python
for variable in (1, 2.0, 3, 4.0):
    print(type(variable))
```

    <class 'int'>
    <class 'float'>
    <class 'int'>
    <class 'float'>

> `3`. `iterable = (1, "dog", 0, 3, 4.0)`

```python
for variable in (1, "dog", 0, 3, 4.0):
    print(type(variable))
```

    <class 'int'>
    <class 'str'>
    <class 'int'>
    <class 'int'>
    <class 'float'>

## Question 2

> `2`. Consider the following polynomial:

> $$ 3 n ^ 3 - 183n ^ 2 + 3318n - 18757 $$

> `1`. Use the `sympy.isprime` function to find the lowest positive integer value
> of $n$ for which the absolute value of that polynomial is not prime?

Start by defining the cubic:

```python
import sympy as sym

def cubic(n):
    """
    Return the value of the absolute value of the cubic for the given value of n
    """
    return abs(3 * n ** 3 - 183 * n ** 2 + 3318 * n - 18757)
```

Increment `n` until `cubic(n)` is no longer prime:

```python
n = 1
while sym.isprime(cubic(n)) is True:
    n += 1

n
```

    47

> `2`. How many **unique** primes up until the first non prime value are there?
> (Hint: the `set` tool might prove useful here.)

```python
primes = [cubic(n_value) for n_value in range(1, n)]
unique_primes = set(primes)
unique_primes
```

    {37,
     41,
     59,
     109,
     229,
     409,
     419,
     499,
     739,
     829,
     877,
     1201,
     1531,
     1597,
     1669,
     1823,
     1999,
     2389,
     2749,
     2917,
     3061,
     3271,
     3307,
     3469,
     3491,
     3529,
     4789,
     5441,
     6367,
     7691,
     8221,
     10259,
     10369,
     12829,
     13163,
     15619,
     16421,
     20051,
     24071,
     28499,
     33353,
     38651}

Let us count them:

```python
len(unique_primes)
```

    42

## Question 3

> `3`. Check the following identify for each value of $n\in\{0, 10, 100, 2000\}$:
>  $ \sum_{i=0}^n i=\frac{n(n+1)}{2} $

Define a function to check the identity:

```python
def check_identity(n):
    """
    Computes lhs and the rhs of the given identity.
    """
    lhs = sum(i for i in range(n + 1))
    rhs = n * (n + 1) / 2
    return lhs == rhs
```

Checks the identify for the given values:

```python
all(check_identity(n) for n in (0, 10, 100, 2000))
```

    True

## Question 4

> `4`. Check the following identify for all positive integer values of $n$ less
> than 5000: $ \sum_{i=0}^n i^2=\frac{n(n+1)(2n+1)}{6} $

Define a function to check the identity:

```python
def check_identity(n):
    """
    Computes lhs and the rhs of the given identity.
    """
    lhs = sum(i ** 2 for i in range(n + 1))
    rhs = n * (n + 1) * (2 * n + 1) / 6
    return lhs == rhs
```

Checks the identify for the given values:

```python
all(check_identity(n) for n in range(1, 5001))
```

    True

## Question 5

> `5`. Repeat the experiment of selecting a random integer between 0 and 10
> until it is even 1000 times (see
> {ref}`repeat_code_while_a_given_condition_holds`).  What is the average number
> of times taken to select an even number?

Write a function to repeat the code from
{ref}`repeat_code_while_a_given_condition_holds`:

```python
import random

def count_number_of_selections_until_even(seed):
    """
    Repeatedly sample an integer between 0 and 10 for a given random seed.

    This function returns the number of selections needed.
    """
    random.seed(seed)
    selected_integer = random.randint(0, 10)
    number_of_selections = 1
    while selected_integer % 2 == 1:
        selected_integer = random.randint(0, 10)
        number_of_selections += 1
    return number_of_selections
```

Now use this for 1000 random repetitions (we use each repetition as a seed):

```python
number_of_selections = [
    count_number_of_selections_until_even(seed) for seed in range(1000)
]
number_of_selections
```

    [1,
     1,
     1,
     3,
     2,
     2,
     4,
     2,
     3,
     4,
     2,
     2,
     2,
     1,
     3,
     2,
     4,
     1,
     1,
     1,
     1,
     1,
     1,
     1,
     1,
     1,
     2,
     1,
     2,
     1,
     1,
     1,
     3,
     2,
     1,
     1,
     2,
     1,
     1,
     2,
     3,
     1,
     1,
     1,
     1,
     1,
     2,
     3,
     1,
     3,
     2,
     2,
     1,
     4,
     1,
     3,
     1,
     1,
     4,
     3,
     1,
     2,
     2,
     3,
     3,
     1,
     2,
     3,
     2,
     1,
     2,
     2,
     3,
     1,
     2,
     3,
     3,
     1,
     3,
     1,
     1,
     1,
     1,
     4,
     1,
     11,
     1,
     1,
     1,
     3,
     4,
     3,
     1,
     7,
     1,
     1,
     3,
     2,
     3,
     1,
     1,
     3,
     1,
     6,
     1,
     5,
     2,
     4,
     1,
     1,
     1,
     5,
     2,
     1,
     5,
     1,
     2,
     2,
     1,
     1,
     1,
     4,
     1,
     1,
     1,
     4,
     2,
     1,
     2,
     2,
     1,
     3,
     1,
     7,
     2,
     1,
     2,
     2,
     2,
     1,
     2,
     1,
     2,
     1,
     2,
     1,
     1,
     2,
     1,
     3,
     2,
     1,
     1,
     3,
     2,
     2,
     2,
     4,
     1,
     1,
     2,
     1,
     1,
     1,
     2,
     1,
     1,
     4,
     2,
     1,
     8,
     1,
     4,
     3,
     1,
     3,
     1,
     3,
     2,
     4,
     1,
     3,
     7,
     3,
     1,
     2,
     2,
     4,
     2,
     2,
     2,
     1,
     2,
     1,
     4,
     2,
     1,
     1,
     1,
     2,
     1,
     3,
     1,
     2,
     1,
     1,
     1,
     1,
     1,
     1,
     1,
     4,
     1,
     1,
     1,
     1,
     2,
     1,
     1,
     1,
     4,
     2,
     3,
     5,
     5,
     2,
     2,
     1,
     1,
     1,
     1,
     1,
     1,
     1,
     2,
     2,
     1,
     3,
     3,
     3,
     2,
     1,
     2,
     1,
     1,
     1,
     1,
     3,
     1,
     1,
     1,
     2,
     2,
     1,
     2,
     1,
     2,
     2,
     1,
     1,
     3,
     1,
     2,
     1,
     2,
     1,
     2,
     2,
     1,
     1,
     1,
     2,
     4,
     2,
     1,
     4,
     5,
     2,
     1,
     1,
     1,
     1,
     1,
     1,
     1,
     2,
     6,
     1,
     1,
     1,
     1,
     1,
     1,
     5,
     1,
     1,
     2,
     1,
     2,
     4,
     4,
     1,
     1,
     1,
     1,
     1,
     1,
     1,
     1,
     2,
     1,
     1,
     1,
     2,
     4,
     1,
     1,
     2,
     1,
     1,
     2,
     1,
     1,
     2,
     2,
     1,
     3,
     1,
     1,
     2,
     3,
     2,
     2,
     1,
     2,
     1,
     1,
     3,
     2,
     1,
     1,
     2,
     9,
     3,
     1,
     1,
     1,
     4,
     2,
     1,
     2,
     5,
     1,
     3,
     4,
     1,
     1,
     1,
     3,
     2,
     2,
     2,
     1,
     1,
     1,
     1,
     4,
     1,
     3,
     2,
     2,
     1,
     5,
     1,
     2,
     4,
     1,
     2,
     1,
     1,
     1,
     4,
     1,
     1,
     1,
     3,
     2,
     3,
     4,
     1,
     4,
     2,
     5,
     3,
     10,
     3,
     1,
     3,
     1,
     1,
     1,
     3,
     3,
     4,
     2,
     1,
     1,
     1,
     2,
     1,
     1,
     2,
     1,
     1,
     1,
     3,
     1,
     3,
     4,
     2,
     1,
     1,
     1,
     2,
     1,
     1,
     1,
     1,
     1,
     3,
     1,
     1,
     2,
     2,
     3,
     2,
     1,
     1,
     2,
     1,
     3,
     1,
     5,
     1,
     1,
     1,
     2,
     1,
     1,
     4,
     3,
     1,
     3,
     2,
     1,
     1,
     3,
     1,
     3,
     1,
     1,
     5,
     2,
     3,
     1,
     3,
     1,
     1,
     2,
     1,
     1,
     1,
     1,
     3,
     2,
     2,
     2,
     2,
     2,
     1,
     1,
     1,
     1,
     1,
     2,
     1,
     1,
     4,
     2,
     1,
     2,
     2,
     4,
     1,
     1,
     1,
     2,
     1,
     2,
     2,
     2,
     1,
     1,
     1,
     4,
     1,
     2,
     4,
     1,
     1,
     1,
     4,
     1,
     1,
     3,
     1,
     2,
     1,
     2,
     3,
     1,
     2,
     3,
     1,
     2,
     1,
     1,
     1,
     1,
     1,
     3,
     1,
     2,
     1,
     1,
     1,
     2,
     4,
     1,
     1,
     3,
     2,
     3,
     1,
     1,
     2,
     1,
     1,
     1,
     1,
     5,
     1,
     1,
     2,
     1,
     2,
     3,
     1,
     1,
     1,
     1,
     2,
     3,
     1,
     1,
     1,
     2,
     1,
     2,
     2,
     3,
     1,
     2,
     1,
     1,
     1,
     1,
     1,
     1,
     2,
     1,
     1,
     5,
     1,
     2,
     1,
     1,
     2,
     1,
     3,
     1,
     1,
     3,
     4,
     1,
     3,
     1,
     2,
     2,
     1,
     1,
     2,
     1,
     1,
     3,
     1,
     1,
     3,
     1,
     1,
     2,
     1,
     3,
     1,
     1,
     2,
     1,
     3,
     2,
     1,
     2,
     4,
     1,
     1,
     2,
     1,
     4,
     1,
     1,
     1,
     2,
     1,
     3,
     1,
     1,
     5,
     2,
     3,
     1,
     2,
     1,
     1,
     1,
     1,
     3,
     3,
     4,
     1,
     1,
     8,
     2,
     1,
     1,
     3,
     1,
     1,
     1,
     3,
     1,
     1,
     1,
     1,
     1,
     5,
     1,
     1,
     2,
     2,
     3,
     1,
     4,
     2,
     1,
     1,
     1,
     3,
     1,
     1,
     3,
     1,
     4,
     1,
     1,
     1,
     1,
     1,
     1,
     1,
     1,
     2,
     3,
     1,
     1,
     4,
     2,
     3,
     1,
     1,
     3,
     3,
     1,
     1,
     2,
     1,
     4,
     1,
     2,
     1,
     2,
     1,
     1,
     3,
     1,
     1,
     5,
     1,
     1,
     2,
     1,
     1,
     1,
     1,
     2,
     1,
     1,
     3,
     1,
     1,
     1,
     2,
     1,
     2,
     1,
     1,
     1,
     1,
     1,
     1,
     1,
     4,
     2,
     2,
     1,
     1,
     1,
     2,
     1,
     1,
     2,
     2,
     2,
     1,
     1,
     4,
     1,
     1,
     2,
     1,
     3,
     1,
     1,
     3,
     1,
     2,
     1,
     1,
     1,
     3,
     2,
     1,
     1,
     1,
     2,
     1,
     1,
     2,
     3,
     6,
     1,
     2,
     3,
     3,
     1,
     1,
     1,
     1,
     2,
     1,
     3,
     2,
     1,
     1,
     2,
     2,
     1,
     1,
     1,
     2,
     1,
     2,
     1,
     10,
     1,
     2,
     2,
     4,
     2,
     3,
     3,
     1,
     3,
     2,
     1,
     1,
     2,
     1,
     1,
     1,
     2,
     2,
     3,
     2,
     2,
     4,
     1,
     1,
     1,
     2,
     2,
     2,
     3,
     1,
     1,
     3,
     1,
     2,
     1,
     1,
     1,
     3,
     2,
     2,
     3,
     1,
     1,
     1,
     4,
     1,
     2,
     1,
     4,
     2,
     2,
     1,
     3,
     2,
     3,
     2,
     1,
     5,
     1,
     3,
     3,
     2,
     1,
     1,
     3,
     1,
     1,
     1,
     4,
     2,
     1,
     1,
     1,
     1,
     1,
     2,
     1,
     2,
     3,
     1,
     1,
     1,
     1,
     2,
     2,
     2,
     2,
     1,
     1,
     1,
     2,
     1,
     3,
     1,
     4,
     1,
     1,
     1,
     5,
     1,
     1,
     1,
     2,
     1,
     1,
     1,
     1,
     2,
     4,
     2,
     1,
     2,
     1,
     1,
     1,
     1,
     1,
     1,
     3,
     3,
     1,
     2,
     5,
     2,
     1,
     1,
     1,
     2,
     1,
     1,
     1,
     1,
     3,
     1,
     1,
     1,
     4,
     2,
     2,
     2,
     1,
     1,
     1,
     1,
     1,
     2,
     1,
     2,
     3,
     1,
     2,
     1,
     1,
     1,
     1,
     2,
     5,
     4,
     1,
     1,
     1,
     1,
     1,
     5,
     1,
     1,
     2,
     1,
     1,
     1,
     2,
     1,
     2,
     2,
     2,
     1,
     2,
     2,
     1,
     2,
     1,
     1,
     1,
     4,
     4,
     1,
     1,
     2,
     1,
     1,
     3,
     1,
     1,
     1,
     7,
     3,
     1,
     1]

We will use `numpy` which has a `mean` function:

```python
import numpy as np

np.mean(number_of_selections)
```

    1.839


## Two-dimensional advection equation

We want to solve the following PDE:

\begin{equation}
    \frac{\partial \phi}{\partial t} + u \frac{\partial \phi}{\partial x} + v \frac{\partial \phi}{\partial y} = 0
\end{equation}

The independen variables (i.e, $x$, $y$ and $t$) are used as input values for the NN, and the solution (i.e. $\phi(x,y,t)$) is the output. In order to find the solution, at each step the NN outputs are derived w.r.t the inputs. Then, a loss function that matches the PDE is built and the weights are updated accordingly. If the loss function goes to zero, we can assume that our NN is indeed the solution to our PDE. We will try to find a general solution for different values of $u$, so it will be set also as an input.

```
# autoreload nangs
%reload_ext autoreload
%autoreload 2
%matplotlib inline
```

```
import math
import numpy as np
import matplotlib.pyplot as plt
import torch
```

First we define our PDE and set the values for training.

```
from nangs.pde import PDE
from nangs.bocos import PeriodicBoco, DirichletBoco
from nangs.solutions import MLP

class MyPDE(PDE):
    def __init__(self, inputs=None, outputs=None, params=None):
        super().__init__(inputs, outputs, params)
    def computePDELoss(self, grads, inputs, outputs, params):
        dpdt, dpdx, dpdy = grads['p']['t'], grads['p']['x'], grads['p']['y']
        u, v = params['u'], params['v']
        return [dpdt + u*dpdx + v*dpdy]

# instanciate pde
pde = MyPDE(inputs=['x', 'y', 't'], outputs=['p'], params=['u', 'v'])

# define input values
x = np.linspace(0,1,20)
y = np.linspace(0,1,20)
t = np.linspace(0,1,10)
u = np.array([1.0])
v = np.array([1.0])

pde.setValues({'x': x, 'y': y, 't': t[:-1], 'u': u, 'v': v})
#pde.setValues({'x': x, 'y': y, 't': t}, train=False)
```

Boundary conditions.

```
# periodic b.c for the space dimension
x1, x2 = np.array([0]), np.array([1])
boco = PeriodicBoco('boco_x', {'x': x[:1], 'y': y, 't': t[:-1]}, {'x': x[-1:], 'y':y, 't': t[:-1]})
pde.addBoco(boco)

y1, y2 = np.array([0]), np.array([1])
boco = PeriodicBoco('boco_y', {'x': x, 'y': y[:1], 't': t[:-1]}, {'x': x, 'y':y[-1:], 't': t[:-1]})
pde.addBoco(boco)

# initial condition (dirichlet for temporal dimension)
p0 = np.zeros((len(y)*len(x)))
for i, _y in enumerate(y):
    for j, _x in enumerate(x):
        p0[i*len(x) + j] = math.sin(2.*math.pi*_x)*math.sin(2.*math.pi*_y)

boco = DirichletBoco('initial_condition', {'x': x, 'y': y, 't': t[:1]}, {'p': p0})
pde.addBoco(boco)
```

```
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt
from matplotlib import cm

fig = plt.figure()
ax = fig.gca(projection='3d')
X, Y = np.meshgrid(x, y)
surf = ax.plot_surface(X, Y, p0.reshape((len(y),len(x))), cmap=cm.coolwarm, linewidth=0, antialiased=False)
fig.colorbar(surf, shrink=0.5, aspect=5)
plt.show()
```

Now we define a topology for our solution and set the training parameters. Then we can find a solution for our PDE.

```
# define solution topology
mlp = MLP(pde.n_inputs, pde.n_outputs, 5, 2048)
optimizer = torch.optim.Adam(mlp.parameters(), lr=3e-4)
pde.compile(mlp, optimizer)
```

```
# find the solution
hist = pde.solve(epochs=50)
```

Epoch 1/50 Losses 0.19511
 PDE  [ 0.00556 ]
 boco_x 0.00507
 boco_y 0.00732
 initial_condition 0.17716
Epoch 2/50 Losses 0.13398
 PDE  [ 0.01300 ]
 boco_x 0.01200
 boco_y 0.01220
 initial_condition 0.09678
Epoch 3/50 Losses 0.09550
 PDE  [ 0.01335 ]
 boco_x 0.01441
 boco_y 0.01595
 initial_condition 0.05179
Epoch 4/50 Losses 0.07174
 PDE  [ 0.01067 ]
 boco_x 0.01420
 boco_y 0.01389
 initial_condition 0.03298
Epoch 5/50 Losses 0.05106
 PDE  [ 0.01051 ]
 boco_x 0.01109
 boco_y 0.01132
 initial_condition 0.01815
Epoch 6/50 Losses 0.03697
 PDE  [ 0.00884 ]
 boco_x 0.00846
 boco_y 0.00853
 initial_condition 0.01114
Epoch 7/50 Losses 0.02623
 PDE  [ 0.00519 ]
 boco_x 0.00710
 boco_y 0.00691
 initial_condition 0.00703
Epoch 8/50 Losses 0.01907
 PDE  [ 0.00502 ]
 boco_x 0.00508
 boco_y 0.00482
 initial_condition 0.00416
Epoch 9/50 Losses 0.01412
 PDE  [ 0.00417 ]
 boco_x 0.00363
 boco_y 0.00362
 initial_condition 0.00270
Epoch 10/50 Losses 0.01249
 PDE  [ 0.00303 ]
 boco_x 0.00356
 boco_y 0.00349
 initial_condition 0.00241
Epoch 11/50 Losses 0.01129
 PDE  [ 0.00286 ]
 boco_x 0.00314
 boco_y 0.00310
 initial_condition 0.00219
Epoch 12/50 Losses 0.00714
 PDE  [ 0.00245 ]
 boco_x 0.00177
 boco_y 0.00161
 initial_condition 0.00130
Epoch 13/50 Losses 0.00652
 PDE  [ 0.00210 ]
 boco_x 0.00168
 boco_y 0.00156
 initial_condition 0.00117
Epoch 14/50 Losses 0.01129
 PDE  [ 0.00230 ]
 boco_x 0.00345
 boco_y 0.00352
 initial_condition 0.00202
Epoch 15/50 Losses 0.01021
 PDE  [ 0.00219 ]
 boco_x 0.00295
 boco_y 0.00317
 initial_condition 0.00191
Epoch 16/50 Losses 0.00911
 PDE  [ 0.00193 ]
 boco_x 0.00271
 boco_y 0.00275
 initial_condition 0.00172
Epoch 17/50 Losses 0.00631
 PDE  [ 0.00184 ]
 boco_x 0.00168
 boco_y 0.00169
 initial_condition 0.00110
Epoch 18/50 Losses 0.00708
 PDE  [ 0.00167 ]
 boco_x 0.00206
 boco_y 0.00204
 initial_condition 0.00131
Epoch 19/50 Losses 0.00418
 PDE  [ 0.00160 ]
 boco_x 0.00095
 boco_y 0.00094
 initial_condition 0.00068
Epoch 20/50 Losses 0.01423
 PDE  [ 0.00186 ]
 boco_x 0.00455
 boco_y 0.00435
 initial_condition 0.00347
Epoch 21/50 Losses 0.00487
 PDE  [ 0.00164 ]
 boco_x 0.00122
 boco_y 0.00122
 initial_condition 0.00078
Epoch 22/50 Losses 0.00344
 PDE  [ 0.00127 ]
 boco_x 0.00079
 boco_y 0.00086
 initial_condition 0.00052
Epoch 23/50 Losses 0.00388
 PDE  [ 0.00143 ]
 boco_x 0.00093
 boco_y 0.00098
 initial_condition 0.00053
Epoch 24/50 Losses 0.00559
 PDE  [ 0.00129 ]
 boco_x 0.00179
 boco_y 0.00164
 initial_condition 0.00086
Epoch 25/50 Losses 0.00647
 PDE  [ 0.00127 ]
 boco_x 0.00203
 boco_y 0.00211
 initial_condition 0.00106
Epoch 26/50 Losses 0.00485
 PDE  [ 0.00119 ]
 boco_x 0.00145
 boco_y 0.00139
 initial_condition 0.00082
Epoch 27/50 Losses 0.00530
 PDE  [ 0.00127 ]
 boco_x 0.00159
 boco_y 0.00158
 initial_condition 0.00086
Epoch 28/50 Losses 0.00318
 PDE  [ 0.00105 ]
 boco_x 0.00081
 boco_y 0.00083
 initial_condition 0.00050
Epoch 29/50 Losses 0.00439
 PDE  [ 0.00098 ]
 boco_x 0.00134
 boco_y 0.00130
 initial_condition 0.00076
Epoch 30/50 Losses 0.00625
 PDE  [ 0.00109 ]
 boco_x 0.00194
 boco_y 0.00200
 initial_condition 0.00122
Epoch 31/50 Losses 0.00549
 PDE  [ 0.00098 ]
 boco_x 0.00184
 boco_y 0.00166
 initial_condition 0.00101
Epoch 32/50 Losses 0.00588
 PDE  [ 0.00109 ]
 boco_x 0.00188
 boco_y 0.00192
 initial_condition 0.00099
Epoch 33/50 Losses 0.00504
 PDE  [ 0.00107 ]
 boco_x 0.00145
 boco_y 0.00161
 initial_condition 0.00091
Epoch 34/50 Losses 0.00582
 PDE  [ 0.00126 ]
 boco_x 0.00166
 boco_y 0.00187
 initial_condition 0.00103
Epoch 35/50 Losses 0.00531
 PDE  [ 0.00108 ]
 boco_x 0.00164
 boco_y 0.00170
 initial_condition 0.00088
Epoch 36/50 Losses 0.00982
 PDE  [ 0.00130 ]
 boco_x 0.00338
 boco_y 0.00307
 initial_condition 0.00207
Epoch 37/50 Losses 0.00637
 PDE  [ 0.00140 ]
 boco_x 0.00182
 boco_y 0.00186
 initial_condition 0.00129
Epoch 38/50 Losses 0.00398
 PDE  [ 0.00105 ]
 boco_x 0.00116
 boco_y 0.00111
 initial_condition 0.00066
Epoch 39/50 Losses 0.00363
 PDE  [ 0.00097 ]
 boco_x 0.00104
 boco_y 0.00105
 initial_condition 0.00057
Epoch 40/50 Losses 0.00212
 PDE  [ 0.00080 ]
 boco_x 0.00049
 boco_y 0.00052
 initial_condition 0.00031
Epoch 41/50 Losses 0.00187
 PDE  [ 0.00073 ]
 boco_x 0.00045
 boco_y 0.00044
 initial_condition 0.00024
Epoch 42/50 Losses 0.00271
 PDE  [ 0.00066 ]
 boco_x 0.00077
 boco_y 0.00072
 initial_condition 0.00056
Epoch 43/50 Losses 0.00316
 PDE  [ 0.00065 ]
 boco_x 0.00096
 boco_y 0.00098
 initial_condition 0.00058
Epoch 44/50 Losses 0.00266
 PDE  [ 0.00070 ]
 boco_x 0.00073
 boco_y 0.00076
 initial_condition 0.00047
Epoch 45/50 Losses 0.00492
 PDE  [ 0.00069 ]
 boco_x 0.00165
 boco_y 0.00168
 initial_condition 0.00090
Epoch 46/50 Losses 0.00928
 PDE  [ 0.00126 ]
 boco_x 0.00289
 boco_y 0.00311
 initial_condition 0.00203
Epoch 47/50 Losses 0.00691
 PDE  [ 0.00111 ]
 boco_x 0.00230
 boco_y 0.00217
 initial_condition 0.00134
Epoch 48/50 Losses 0.00740
 PDE  [ 0.00109 ]
 boco_x 0.00229
 boco_y 0.00237
 initial_condition 0.00165
Epoch 49/50 Losses 0.00239
 PDE  [ 0.00069 ]
 boco_x 0.00071
 boco_y 0.00063
 initial_condition 0.00036
Epoch 50/50 Losses 0.00186
 PDE  [ 0.00065 ]
 boco_x 0.00048
 boco_y 0.00044
 initial_condition 0.00028

    /opt/conda/lib/python3.7/site-packages/numpy/core/fromnumeric.py:3257: RuntimeWarning: Mean of empty slice.
      out=out, **kwargs)
    /opt/conda/lib/python3.7/site-packages/numpy/core/_methods.py:161: RuntimeWarning: invalid value encountered in double_scalars
      ret = ret.dtype.type(ret / rcount)

```
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15,5))
ax1.plot(hist['train_loss'], label="train_loss")
ax1.grid(True)
ax1.legend()
ax1.set_yscale("log")
for boco in pde.bocos:
    ax2.plot(hist['bocos'][boco.name], label=boco.name)
ax2.legend()
ax2.grid(True)
ax2.set_yscale("log")
plt.show()
```

Finally, we can evaluate our solution.

```
# evaluate the solution
x = np.linspace(0,1,40)
y = np.linspace(0,1,40)
t = np.linspace(0,1,40)
u, v = np.array([1]), np.array([1])
p, p0, l2 = [], [], []
for _t in t:
    _p0 = np.zeros((len(y)*len(x)))
    for i, _y in enumerate(y):
        for j, _x in enumerate(x):
            _p0[i*len(x) + j] = math.sin(2.*math.pi*(_x-u*_t))*math.sin(2.*math.pi*(_y-v*_t))
    pde.evaluate({'x': x, 'y': y, 't': np.array([_t])})
    _p = pde.outputs['p']
    _l2 = np.sqrt(np.mean((_p - _p0)**2))
    p.append(_p)
    p0.append(_p0)
    l2.append(_l2)

from matplotlib import animation, rc
rc('animation', html='html5')

def plot(ax1, ax2, ax3, p, p0, t, l2):
    ax1.clear()
    ax2.clear()
    ax3.clear()
    tit1 = ax1.set_title(f't = {t:.2f} (NN)')
    tit2 = ax2.set_title(f't = {t:.2f} (Exact)')
    tit3 = ax3.set_title(f't = {t:.2f} (l2 {l2:5f})')
    X, Y = np.meshgrid(x, y)
    ax1.plot_surface(X, Y, p.reshape((len(y),len(x))), cmap=cm.coolwarm, linewidth=0, antialiased=False)
    ax1.set_xlim([0,1])
    ax1.set_ylim([0,1])
    ax1.set_zlim([-1,1])
    ax2.plot_surface(X, Y, p0.reshape((len(y),len(x))), cmap=cm.coolwarm,  linewidth=0, antialiased=False)
    ax2.set_xlim([0,1])
    ax2.set_ylim([0,1])
    ax2.set_zlim([-1,1])
    ax3.plot_surface(X, Y, p.reshape((len(y),len(x)))-p0.reshape((len(y),len(x))), cmap=cm.coolwarm,  linewidth=0, antialiased=False)
    ax3.set_xlim([0,1])
    ax3.set_ylim([0,1])
    ax3.set_zlim([-0.1,0.1])
    return [tit1, tit2, tit3]

def get_anim(fig, ax1, ax2, ax3, p, p0, t, l2):
    def anim(i):
        return plot(ax1, ax2, ax3, p[i], p0[i], t[i], l2[i])
    return anim

fig = plt.figure(figsize=(20,5))
ax1 = fig.add_subplot(1, 3, 1, projection='3d')
ax2 = fig.add_subplot(1, 3, 2, projection='3d')
ax3 = fig.add_subplot(1, 3, 3, projection='3d')
animate = get_anim(fig, ax1, ax2, ax3, p, p0, t, l2)
anim = animation.FuncAnimation(fig, animate, frames=len(t), interval=300, blit=True)
```

```
anim
```

```

```


```
import sympy
from sympy import Symbol, diff, Matrix, pprint, MatrixSymbol, symbols

def grad(fun, coords):
    return Matrix([diff(fun, coord) for coord in coords])

def hess(fun, coords):
    dfun = grad(fun, coords)
    return Matrix([ [diff(dfun[i], coord) for coord in coords]
                    for i in range(len(dfun))])

def Lc(fun, hfuns, lams, c):
    retfun = fun
    for i in range(len(lams)):
        retfun = retfun + lams[i]*hfuns[i] + c/2*hfuns[i]**2
    return retfun

def dLc(fun, hfuns, lams, c, coords):
    retfun = grad(fun, coords)
    for i in range(len(lams)):
        retfun += lams[i] * grad(hfuns[i], coords) + c*grad(hfuns[i], coords)*hfuns[i]
    return retfun

def ddLc(fun, hfuns, lams, c, coords):
    dfun = dLc(fun, hfuns, lams, c, coords)
    return Matrix([ [diff(dfun[i], coord) for coord in coords]
                    for i in range(len(dfun))])

```

```
b = Symbol('b', real = True, positive = True)
a = Symbol('a', real = True)
lam = Symbol('λ', real = True)
c = Symbol('c', real = True, positive=True)
x = Symbol('x', real = True)
y = Symbol('y', real = True)

def print_fun(fun, hfuns, lams, coords):
    print('f(x, y):')
    pprint(fun)
    print('grad(f(x, y)):')
    pprint(grad(fun, coords))
    print('hess(f(x, y)):')
    pprint(hess(fun, coords))
    for i in range(len(hfuns)):
        print('')
        print('h_{}(x, y):'.format(i))
        pprint(hfuns[i])
        print('grad(h_{}(x, y)):'.format(i))
        pprint(grad(hfuns[i], coords))
        print('hess(h_{}(x, y)):'.format(i))
        pprint(hess(hfuns[i], coords))
    print('')
    print('L(x, y, lam, c):')
    pprint(Lc(f, hfuns, lams, c))
    print('dL(x, y, lam, c):')
    pprint(dLc(f, hfuns, lams, c, coords))
    print('ddL(x, y, lam, c):')
    pprint(ddLc(f, hfuns, lams, c, coords))

f = b*x
h = x+y*y -a
print_fun(f, [h], [lam], [x, y])
```

    f(x, y):
    b⋅x
    grad(f(x, y)):
    ⎡b⎤
    ⎢ ⎥
    ⎣0⎦
    hess(f(x, y)):
    ⎡0  0⎤
    ⎢    ⎥
    ⎣0  0⎦

    h_0(x, y):
              2
    -a + x + y
    grad(h_0(x, y)):
    ⎡ 1 ⎤
    ⎢   ⎥
    ⎣2⋅y⎦
    hess(h_0(x, y)):
    ⎡0  0⎤
    ⎢    ⎥
    ⎣0  2⎦

    L(x, y, lam, c):
                         2
            ⎛          2⎞
          c⋅⎝-a + x + y ⎠      ⎛          2⎞
    b⋅x + ──────────────── + λ⋅⎝-a + x + y ⎠
                 2
    dL(x, y, lam, c):
    ⎡        ⎛          2⎞      ⎤
    ⎢  b + c⋅⎝-a + x + y ⎠ + λ  ⎥
    ⎢                           ⎥
    ⎢      ⎛          2⎞        ⎥
    ⎣2⋅c⋅y⋅⎝-a + x + y ⎠ + 2⋅y⋅λ⎦
    ddL(x, y, lam, c):
    ⎡  c                 2⋅c⋅y              ⎤
    ⎢                                       ⎥
    ⎢            2       ⎛          2⎞      ⎥
    ⎣2⋅c⋅y  4⋅c⋅y  + 2⋅c⋅⎝-a + x + y ⎠ + 2⋅λ⎦

```
# example 20.5
f = (2*(x**2 + y**2 - 1) - x)/1e4
h = [(x**2 + y**2 - 1)]
print_fun(f, h, symbols('λ:1'), [x, y])
```

    f(x, y):
            2                         2
    0.0002⋅x  - - -0.0001⋅x + 0.0002⋅y  - 0.0002
    grad(f(x, y)):
    ⎡0.0004⋅x - 0.0001⎤
    ⎢                 ⎥
    ⎣    0.0004⋅y     ⎦
    hess(f(x, y)):
    ⎡0.0004    0   ⎤
    ⎢              ⎥
    ⎣  0     0.0004⎦

    h_0(x, y):
     2    2
    x  + y  - 1
    grad(h_0(x, y)):
    ⎡2⋅x⎤
    ⎢   ⎥
    ⎣2⋅y⎦
    hess(h_0(x, y)):
    ⎡2  0⎤
    ⎢    ⎥
    ⎣0  2⎦

    L(x, y, lam, c):
                   2
      ⎛ 2    2    ⎞
    c⋅⎝x  + y  - 1⎠            2                         2      ⎛ 2    2    ⎞
    ──────────────── + 0.0002⋅x  - - -0.0001⋅x + 0.0002⋅y  + λ0⋅⎝x  + y  - 1⎠ - 0.
           2

    0002

    dL(x, y, lam, c):
    ⎡      ⎛ 2    2    ⎞                             ⎤
    ⎢2⋅c⋅x⋅⎝x  + y  - 1⎠ + 2⋅x⋅λ0 + 0.0004⋅x - 0.0001⎥
    ⎢                                                ⎥
    ⎢          ⎛ 2    2    ⎞                         ⎥
    ⎣    2⋅c⋅y⋅⎝x  + y  - 1⎠ + 2⋅y⋅λ0 + 0.0004⋅y     ⎦
    ddL(x, y, lam, c):
    ⎡     2       ⎛ 2    2    ⎞
    ⎢4⋅c⋅x  + 2⋅c⋅⎝x  + y  - 1⎠ + 2⋅λ0 + 0.0004                   4⋅c⋅x⋅y
    ⎢
    ⎢                                                 2       ⎛ 2    2    ⎞
    ⎣                 4⋅c⋅x⋅y                    4⋅c⋅y  + 2⋅c⋅⎝x  + y  - 1⎠ + 2⋅λ0

             ⎤
             ⎥
             ⎥
             ⎥
     + 0.0004⎦

```
sympy.pretty(f)
```

    '        2                         2         \n0.0002⋅x  - - -0.0001⋅x + 0.0002⋅y  - 0.0002'

```
from sympy import pretty
import numpy as np
import scipy.optimize
class augmented_algo(object):
    def __init__(self, fun, h_list, coords, coords0):
        self.fun = fun
        self.hfuns = h_list
        self.h_size = len(self.hfuns)
        self.coords = coords
        self.coords0 = coords0
        self.lams = symbols('λ:{}'.format(len(self.hfuns)))
        self.c = symbols('c', positive=True)
        self.Lc = Lc(self.fun, self.hfuns, self.lams, self.c)
        self.dLc = dLc(self.fun, self.hfuns, self.lams, self.c, self.coords)
        self.ddLc = ddLc(self.fun, self.hfuns, self.lams, self.c, self.coords)

        self.counter = 0

        self.x_val = self.coords0
        self.c_val = 10.
        self.lam_val = [0. for _ in range(self.h_size)]
        self.tau_val = 10
        self.alpha_val = .1
        self.beta_val = .9
        self.eta0_val = .1258925
        self.eta_val = 1/self.c_val**self.alpha_val
        self.eps_val = 1e-8
        self.eps_0_val = 1/self.c_val
        self.eps_k_val = self.eps_0_val
        self.iterates = list()

    def __repr__(self):
        fun_str = pretty(self.fun)
        hfun_strs = [pretty(h) for h in self.hfuns]
        lag_str = pretty(self.Lc)
        outstr = []
        coord_str = ', '.join((pretty(c) for c in self.coords))
        outstr.append('f({}) ='.format(coord_str))
        outstr.append(fun_str)
        outstr.append('')
        outstr.append('h({}) ='.format(coord_str))
        for hf in hfun_strs:
            outstr.append(hf)
        outstr.append('')
        outstr.append('L_c(({}), ({})) = '.format(
            coord_str, ', '.join((pretty(c) for c in self.lams))))
        outstr.append(lag_str)
        return '\n'.join(outstr)

    def numeric_Lc(self):
        subs = {lam: lam_val for lam, lam_val in zip(self.lams, self.lam_val)}
        subs[self.c] = self.c_val
        fun_val = sympy.utilities.lambdify(
            self.coords,
            self.Lc.subs(subs),
            modules='numpy')

        grad_val = sympy.utilities.lambdify(
            self.coords,
            self.dLc.subs(subs),
            modules='numpy')

        hess_val = sympy.utilities.lambdify(
            self.coords,
            self.ddLc.subs(subs),
            modules='numpy')

        h_vals = [sympy.utilities.lambdify(self.coords, self.hfuns[i], modules='numpy')
                  for i in range(self.h_size)]
        return fun_val, grad_val, hess_val, h_vals

    def iteration(self):
        self.counter += 1
        print('\nIteration {}:'.format(self.counter))
        fun_val, grad_val, hess_val,  h_vals = self.numeric_Lc()
        # 1 solve local prob
        result = scipy.optimize.minimize(
            lambda x: fun_val(*x), self.x_val, tol = self.eps_k_val,
            method='Newton-CG',# 'trust-ncg',#
            jac=lambda x: np.asarray(grad_val(*x)).flatten(),
            hess=lambda x: np.asarray(hess_val(*x)).squeeze())
        print('success = {}'.format(result.success))
        print('message = {}'.format(result.message))
        print('solution = {}'.format(result.x))
        if result.success:
            self.x_val = result.x
        else:
            raise Exception(result.message)

        # 2 test convergence
        gv = grad_val(*self.x_val)
        gv = np.sqrt(float(gv.T*gv))
        grad_convergence =  gv < self.eps_val
        h_val_evals = [h(*self.x_val)**2 for h in h_vals]
        hv = np.sqrt(sum(h_val_evals))
        constraints_convergence = hv < self.eps_val
        print('\nConvergence:')
        print(('grad_convergence:        {} ({:.4e} >= {}),\n'
               'constraints_convergence: {} ({:.4e} >= {})').format(
            grad_convergence, gv, self.eps_val, constraints_convergence, hv, self.eps_val))
        print('overall convergence: {}, current tol = {:.4e}'.format(
            grad_convergence and constraints_convergence, self.eps_k_val))
        overall_convergence = grad_convergence and constraints_convergence

        if hv < self.eta_val:
            self.lam_val = [lam + self.c_val*h_eval
                            for lam, h_eval in zip(self.lam_val, h_val_evals)]
            self.eps_k_val /= self.c_val
            self.eta_val /= self.c_val**self.beta_val
            print(('\nWeak constraint violation: {:.4e} < {:.4e}; '
                   'updated multipliers').format(
                hv, self.eta_val))
            print('λ = {}, tol_k = {:.4e}, update_tol = {:.4e}'.format(
                ['{:.4e}'.format(l) for l in self.lam_val], self.eps_k_val, self.eta_val))
        else:
            self.c_val *= self.tau_val
            self.eps_k_val = self.eps_0_val/self.c_val
            self.eta_val = self.eta0_val/self.c_val**self.beta_val
            print(('\nBad constraint violation: {:.4e} > {:.4e}; '
                   'increased penalty').format(
                hv, self.eta_val))
            print('c = {:.4e}, tol_k = {:.4e}, update_tol = {:.4e}'.format(
                self.c_val, self.eps_k_val, self.eta_val))

        self.iterates.append(scipy.optimize.OptimizeResult(
            {'x': self.x_val.copy(),
             'success': result.success,
             'message': result.message,
             'fun': result.fun,
             'jac': result.jac,
             'hess': hess_val(*self.x_val)}))
        return overall_convergence

```

```
aa = augmented_algo(f, h, [x, y], (-.1, 1.))
fun_val, grad_val, hess_val, h_vals = aa.numeric_Lc()
print(hess_val(0, 0))
converged = False
while not converged:
    try:
        converged = aa.iteration()
    except Exception:
        converged = True

```

    message = Desired error not necessarily achieved due to precision loss.
    solution = [  9.99999974e-01   2.27360443e-04]

    message = Optimization terminated successfully.
    solution = [ 0.99999761  0.00218767]

    Convergence:
    grad_convergence:        False (2.1877e-07 >= 1e-08),
    constraints_convergence: True (1.5050e-11 >= 1e-08)
    overall convergence: False, current tol = 1.0000e-08

    Weak constraint violation: 1.5050e-11 < 3.1623e-14; updated multipliers
    λ = ['4.9927e-07'], tol_k = 1.0000e-15, update_tol = 3.1623e-14

    Iteration 21:
    success = False
    message = Optimization terminated successfully.
    solution = [ 0.99999579  0.0029033 ]

    Convergence:
    grad_convergence:        False (2.9033e-07 >= 1e-08),
    constraints_convergence: True (1.5050e-10 >= 1e-08)
    overall convergence: False, current tol = 1.0000e-07

    Weak constraint violation: 1.5050e-10 < 1.9953e-12; updated multipliers
    λ = ['4.9927e-07'], tol_k = 1.0000e-13, update_tol = 1.9953e-12

    Iteration 19:
    success = True
    message = Optimization terminated successfully.
    solution = [ 0.99999761  0.00218767]

    Convergence:
    grad_convergence:        False (2.1877e-07 >= 1e-08),
    constraints_convergence: True (1.5050e-10 >= 1e-08)
    overall convergence: False, current tol = 1.0000e-13

    Bad constraint violation: 1.5050e-10 > 6.3096e-08; increased penalty
    c = 1.0000e+07, tol_k = 1.0000e-08, update_tol = 6.3096e-08

    Iteration 20:
    success = True
    message = Optimization terminated successfully.
    solution = [ 0.99999578  0.0029033 ]

    Convergence:
    grad_convergence:        False (2.9033e-07 >= 1e-08),
    constraints_convergence: True (1.5050e-09 >= 1e-08)
    overall convergence: False, current tol = 1.0000e-06

    Weak constraint violation: 1.5050e-09 < 1.2589e-10; updated multipliers
    λ = ['4.9927e-07'], tol_k = 1.0000e-11, update_tol = 1.2589e-10

    Iteration 17:
    success = True
    message = Optimization terminated successfully.
    solution = [ 0.99999578  0.0029033 ]

    Convergence:
    grad_convergence:        False (2.9033e-07 >= 1e-08),
    constraints_convergence: True (1.5050e-09 >= 1e-08)
    overall convergence: False, current tol = 1.0000e-11

    Bad constraint violation: 1.5050e-09 > 5.0119e-07; increased penalty
    c = 1.0000e+06, tol_k = 1.0000e-07, update_tol = 5.0119e-07

    Iteration 18:
    success = True
    message = Optimization terminated successfully.
    solution = [ 0.99999469  0.00325557]

    Convergence:
    grad_convergence:        False (3.2556e-07 >= 1e-08),
    constraints_convergence: False (1.5050e-08 >= 1e-08)
    overall convergence: False, current tol = 1.0000e-05

    Weak constraint violation: 1.5050e-08 < 7.9433e-09; updated multipliers
    λ = ['4.9927e-07'], tol_k = 1.0000e-09, update_tol = 7.9433e-09

    Iteration 15:
    success = True
    message = Optimization terminated successfully.
    solution = [ 0.99999578  0.0029033 ]

    Convergence:
    grad_convergence:        False (2.9033e-07 >= 1e-08),
    constraints_convergence: False (1.5050e-08 >= 1e-08)
    overall convergence: False, current tol = 1.0000e-09

    Bad constraint violation: 1.5050e-08 > 3.9811e-06; increased penalty
    c = 1.0000e+05, tol_k = 1.0000e-06, update_tol = 3.9811e-06

    Iteration 16:
    success = True
    message = Optimization terminated successfully.
    solution = [ 0.9999907  0.0042956]

    Convergence:
    grad_convergence:        False (4.2957e-07 >= 1e-08),
    constraints_convergence: False (1.5050e-07 >= 1e-08)
    overall convergence: False, current tol = 1.0000e-04

    Weak constraint violation: 1.5050e-07 < 5.0119e-07; updated multipliers
    λ = ['4.9924e-07'], tol_k = 1.0000e-07, update_tol = 5.0119e-07

    Iteration 12:
    success = True
    message = Optimization terminated successfully.
    solution = [ 0.9999922   0.00393183]

    Convergence:
    grad_convergence:        False (3.9318e-07 >= 1e-08),
    constraints_convergence: False (1.5050e-07 >= 1e-08)
    overall convergence: False, current tol = 1.0000e-07

    Weak constraint violation: 1.5050e-07 < 1.0000e-09; updated multipliers
    λ = ['4.9927e-07'], tol_k = 1.0000e-10, update_tol = 1.0000e-09

    Iteration 13:
    success = True
    message = Optimization terminated successfully.
    solution = [ 0.99999463  0.00325557]

    Convergence:
    grad_convergence:        False (3.2556e-07 >= 1e-08),
    constraints_convergence: False (1.5050e-07 >= 1e-08)
    overall convergence: False, current tol = 1.0000e-10

    Bad constraint violation: 1.5050e-07 > 3.1623e-05; increased penalty
    c = 1.0000e+04, tol_k = 1.0000e-05, update_tol = 3.1623e-05

    Iteration 14:
    success = True
    message = Optimization terminated successfully.
    solution = [ 0.99999002  0.0042956 ]

    Convergence:
    grad_convergence:        False (4.2956e-07 >= 1e-08),
    constraints_convergence: False (1.5050e-06 >= 1e-08)
    overall convergence: False, current tol = 1.0000e-05

    Weak constraint violation: 1.5050e-06 < 5.0119e-07; updated multipliers
    λ = ['4.9922e-07'], tol_k = 1.0000e-07, update_tol = 5.0119e-07

    Iteration 10:
    success = True
    message = Optimization terminated successfully.
    solution = [ 0.99999002  0.0042956 ]

    Convergence:
    grad_convergence:        False (4.2956e-07 >= 1e-08),
    constraints_convergence: False (1.5050e-06 >= 1e-08)
    overall convergence: False, current tol = 1.0000e-07

    Bad constraint violation: 1.5050e-06 > 2.5119e-04; increased penalty
    c = 1.0000e+03, tol_k = 1.0000e-04, update_tol = 2.5119e-04

    Iteration 11:
    success = True
    message = Optimization terminated successfully.
    solution = [ 0.99997826  0.00533145]

    Convergence:
    grad_convergence:        False (5.3316e-07 >= 1e-08),
    constraints_convergence: False (1.5050e-05 >= 1e-08)
    overall convergence: False, current tol = 1.0000e-07

    Bad constraint violation: 1.5050e-05 > 1.9953e-03; increased penalty
    c = 1.0000e+02, tol_k = 1.0000e-03, update_tol = 1.9953e-03

    Iteration 8:
    success = True
    message = Optimization terminated successfully.
    solution = [ 0.99998504  0.00533148]

    Convergence:
    grad_convergence:        False (5.3386e-07 >= 1e-08),
    constraints_convergence: False (1.5049e-06 >= 1e-08)
    overall convergence: False, current tol = 1.0000e-03

    Weak constraint violation: 1.5049e-06 < 3.1623e-05; updated multipliers
    λ = ['4.9900e-07'], tol_k = 1.0000e-05, update_tol = 3.1623e-05

    Iteration 9:
    success = True
    message = Optimization terminated successfully.
    solution = [ 0.99836848  0.05696777]

    Convergence:
    grad_convergence:        False (5.6969e-06 >= 1e-08),
    constraints_convergence: False (1.5059e-05 >= 1e-08)
    overall convergence: False, current tol = 1.0000e-05

    Weak constraint violation: 1.5059e-05 < 2.5119e-05; updated multipliers
    λ = ['4.9650e-07'], tol_k = 1.0000e-06, update_tol = 2.5119e-05

    Iteration 6:
    success = True
    message = Optimization terminated successfully.
    solution = [ 0.99994426  0.00981982]

    Convergence:
    grad_convergence:        False (9.8199e-07 >= 1e-08),
    constraints_convergence: False (1.5050e-05 >= 1e-08)
    overall convergence: False, current tol = 1.0000e-06

    Weak constraint violation: 1.5050e-05 < 3.1623e-06; updated multipliers
    λ = ['4.9877e-07'], tol_k = 1.0000e-07, update_tol = 3.1623e-06

    Iteration 7:
    success = True
    message = Optimization terminated successfully.
    solution = [-0.0863386   0.99625554]

    Convergence:
    grad_convergence:        False (9.9635e-05 >= 1e-08),
    constraints_convergence: False (2.0545e-05 >= 1e-08)
    overall convergence: False, current tol = 1.0000e-03

    Weak constraint violation: 2.0545e-05 < 1.5849e-03; updated multipliers
    λ = ['4.9002e-07'], tol_k = 1.0000e-04, update_tol = 1.5849e-03

    Iteration 4:
    success = True
    message = Optimization terminated successfully.
    solution = [-0.07966411  0.99681147]

    Convergence:
    grad_convergence:        False (9.9695e-05 >= 1e-08),
    constraints_convergence: False (2.0526e-05 >= 1e-08)
    overall convergence: False, current tol = 1.0000e-04

    Weak constraint violation: 2.0526e-05 < 1.9953e-04; updated multipliers
    λ = ['4.9424e-07'], tol_k = 1.0000e-05, update_tol = 1.9953e-04

    Iteration 5:
    success = True[[-19.9996   0.    ]
     [  0.     -19.9996]]

    Iteration 1:
    success = True
    message = Optimization terminated successfully.
    solution = [-0.09950395  0.99506409]

    Convergence:
    grad_convergence:        False (1.4848e-03 >= 1e-08),
    constraints_convergence: False (5.3572e-05 >= 1e-08)
    overall convergence: False, current tol = 1.0000e-01

    Weak constraint violation: 5.3572e-05 < 1.0000e-01; updated multipliers
    λ = ['2.8700e-08'], tol_k = 1.0000e-02, update_tol = 1.0000e-01

    Iteration 2:
    success = True
    message = Optimization terminated successfully.
    solution = [-0.08635119  0.99637205]

    Convergence:
    grad_convergence:        False (4.6862e-03 >= 1e-08),
    constraints_convergence: False (2.1380e-04 >= 1e-08)
    overall convergence: False, current tol = 1.0000e-02

    Weak constraint violation: 2.1380e-04 < 1.2589e-02; updated multipliers
    λ = ['4.8580e-07'], tol_k = 1.0000e-03, update_tol = 1.2589e-02

    Iteration 3:
    success = True

```
%matplotlib inline
import matplotlib.pyplot as plt
```

```
coords = np.array([it.x for it in aa.iterates])
xs = np.linspace(-1.1, 1.1, 51)
ys = np.linspace(-1.1, 1.1, 51)
X, Y = np.meshgrid(xs, ys)
Z = sympy.utilities.lambdify([x, y], f)(X,Y)
CS = plt.contourf(X, Y, Z)

phi = np.linspace(0, 2*np.pi, 97)
xs, ys = np.cos(phi), np.sin(phi)
plt.plot(xs, ys, c='k', lw=2)
plt.plot(coords[:, 0], coords[:, 1], c = 'r', lw = 2)

```

    <matplotlib.figure.Figure at 0x7f5d6741a9b0>

    [<matplotlib.lines.Line2D at 0x7f5d67407358>]

```
a = Symbol('a')
b = Symbol('b')
f = a
f
```

    a

```
f += b
f
```

    a + 7*b

```
a.shape

```

    (3, 1)

```

d = np.array((5., 5))
B = np.matrix(np.eye(2)*1.2)
B
print(np.dot(B, d))
np.dot(d, np.dot(B, d))
```

```

```


#Programação Não Linear - Segundo Projeto Computacional
**Universidade Federal do Ceará**
Professor: RICARDO COELHO
Alunos: MARCIO BARROS OLIVEIRA DE SOUZA,
        PAULINO CARDIAL RABELO e
        PEDRO JORGE DE ABREU FIGUEREDO

#Introdução

Este trabalho consiste no desenvolvimento computacional de métodos de otimização não linear irrestrita. Dentre eles apresentamos o Método de Newton, Gradiente Descente, quase-newton Broyden, Fletcher, Goldfarb e Shanno(BFGS) e Método do Gradiente Conjugado Polak e Ribiére (PR). Primeiramente, apresentamos um pouco sobre o ambiente computacional usado. Em seguida, mostramos a implementação dos métodos em conjunto com um breve resumo de cada um deles. Nas seções "Modo de Usar" e "Parâmetros dos Métodos" mostramos como chamar os métodos e quais os parâmetros implementados. Por fim, fazemos um análise simples encima de um conjunto de dados disponibilizados para o desenvolvimento desta atividade.

#Instalação Ambiente

No desenvolvimento desta atividade íremos utilizar Julia como linguagem de implementação pelo seu facil uso e vasto conjunto de bibliotecas que oferecem suporte a otimização matemática. Para facilitar a distribuição fornecemos um notebook Jupyter que colocamos pode ser usado no Colab Google. Veja que facilmente podemos exportar o mesmo para o uso local.

Para podermos executar corretamente no ambiente Colab temos que executar a seguinte célula e reiniciar a aba do navegador aplicando "F5" quando a célula terminar de instalar o kenerl Julia. No caso do uso local basta ter a aplicação Jupyter Notebook (,no Windows Anaconda) e uma distribuição Julia 1.4.2 ou mais recente.

```julia
# Installation cell
%%shell
if ! command -v julia 3>&1 > /dev/null
then
    wget 'https://julialang-s3.julialang.org/bin/linux/x64/1.4/julia-1.4.2-linux-x86_64.tar.gz' \
        -O /tmp/julia.tar.gz
    tar -x -f /tmp/julia.tar.gz -C /usr/local --strip-components 1
    rm /tmp/julia.tar.gz
fi
julia -e 'using Pkg; pkg"add IJulia; precompile;"'
echo 'Done'
```

Unrecognized magic `%%shell`.

Julia does not use the IPython `%magic` syntax.   To interact with the IJulia kernel, use `IJulia.somefunction(...)`, for example.  Julia macros, string macros, and functions can be used to accomplish most of the other functionalities of IPython magics.

#Dependências

As depências a seguir devem ser instaladas para permitir importar as bibliotecas que são usadas neste trabalho.

```julia
9#Julia 1.4 Environment
using Pkg
pkg"add ForwardDiff; precompile;"
pkg"add Plots; precompile;"
pkg"add PyPlot; precompile;"
```

      Resolving package versions...
       Updating `~/.julia/environments/v1.4/Project.toml`
     [no changes]
       Updating `~/.julia/environments/v1.4/Manifest.toml`
     [no changes]
    Precompiling project...
      Resolving package versions...
       Updating `~/.julia/environments/v1.4/Project.toml`
     [no changes]
       Updating `~/.julia/environments/v1.4/Manifest.toml`
     [no changes]
    Precompiling project...
      Resolving package versions...
       Updating `~/.julia/environments/v1.4/Project.toml`
     [no changes]
       Updating `~/.julia/environments/v1.4/Manifest.toml`
     [no changes]
    Precompiling project...

Funções Auxiliares

Durante a implementação usamos uma biblioteca para calcular derivadas e gradientes através da técnica  de "Automatic Differentiation". Essa permite calcular com precisão e agilidade os métodos aqui utilizados. Além disso, usamos o framework Plots em conjunto com a engine PyPlot para gerar gráficos das funções testadas.

```julia
import ForwardDiff  # calcula derivadas usando automatic differentiation in forward mode
using LinearAlgebra #adiciona operações de algebra linear
using Plots

pyplot()#usa PyPlot como backend engine
∇ = (h,x)->ForwardDiff.gradient(h,x) #gradiente de h(x)
Hessian = (h,x)->ForwardDiff.hessian(h,x) # hessian de h(x)
Df = (h,x)->ForwardDiff.derivative(h,x);  # primeira derivada de h'(x)
D_2f = (h,x)->ForwardDiff.derivative(z->ForwardDiff.derivative(h,z),x) # segunda derivada h"(x)
```

    #72 (generic function with 1 method)

Em seguida mostramos as implementações dos métodos de busca linear que serão usados neste trabalho. Uma analise desses foi feita no [Projeto 1](https://colab.research.google.com/drive/1Nt6ffNGr-3ej0gCgIW2sG5DYSnJ7Pywh?usp=sharing).

> Bloco com recuo

```julia
#função de busca linear
function linear_search(f::Function,x::Vector,d::Vector,method::Function)
    ϕ = t->f(x+t*d)
    α = method(f,x,d)
    return (ϕ(α),x+α*d)
end

function secao_aurea(f::Function,x::Vector,d::Vector;φ::Function = t->f(x+t*d),ρ = 1/2,ϵ = 1e-9)::Float64
    θ_1 = (3 - sqrt(5))/2.0
    θ_2 = (sqrt(5) - 1)/2.0
    #Obtenção intervalo
    a=0
    b=2*ρ
    s = b / 2
    while(φ(b)<φ(s))
        a = s
        s = b
        b *= 2
    end
    #Obtenção de t
    u = a + θ_1*(b - a)
    v = a + θ_2*(b - a)
    while(b - a > ϵ)
        if(φ(u)<φ(v))
            b = v
            v = u
            u = a + θ_1*(b - a)
        else
            a = u
            u = v
            v = a + θ_2*(b - a)
        end
    end
    return (u+v)/2
end

function secao_aurea(f::Function,x::Vector,d::Vector,(a,b)::Tuple{Float64,Float64};φ::Function = t->f(x+t*d),ϵ = 1e-9)::Float64
    θ_1 = (3 - sqrt(5))/2.0
    θ_2 = (sqrt(5) - 1)/2.0
    #Obtenção de t
    u = a + θ_1*(b - a)
    v = a + θ_2*(b - a)
    while(b - a > ϵ)
        if(φ(u)< φ(v))
            b = v
            v = u
            u = a + θ_1*(b - a)
        else
            a = u
            u = v
            v = a + θ_2*(b - a)
        end
    end
    return (u+v)/2
end

function newton(f::Function,x::Vector,d::Vector;φ::Function = y->f(x+y*d),t::Float64=1.0,ϵ = 1e-9,n_iter=1000)::Float64
   iter = 0
   while (abs(Df(φ,t)) > ϵ && iter < n_iter)
         t = t - (Df(φ,t)/D_2f(φ,t))
         iter = iter + 1
   end
   return t
end

function armijo(f::Function,x::Vector,d::Vector;φ::Function = y->f(x+y*d),t::Float64= 1.0,η::Float64 = 0.25)
    # f(x + td) > f(x) +t(∇f(x)⋅ d)
    while (φ(t)>φ(0)+ η*t*(∇(f,x)⋅d))
      t *=0.8
    end
    return t
end
```

    armijo (generic function with 1 method)

#Métodos de Otimização Irrestrita

##Metodo da Gradiente

O método gradiente é um método de descida de primeira ordem que gera uma sequência {$x_{k}$}, globalmente convergente, a partir de um $x_{0}$ qualquer, que irá convergir para $x^*$ .

\

\begin{equation}
x_{k+1}=x_{k}+t_{k}d_{k}=x_{k}-t_{k}\nabla f(x_{k})
\end{equation}
$t_{k}>0$

Na próxima imagem podemos ver um exemplo, onde começando do ponto $A$, e os vetores são os anti-gradiente do ponto, que vamos tomar como a direção para obter o próximo ponto.

Podemos ver na próxima imagem o caminho que a sequência de pontos leva até chegar ao mínimo, com $x_{0}=A$

Lema

  Se $t_{k}$ é obtido por uma minimização local de $f(x_{k}+t_{k}d_{k})$, então $(d_{k+1})^{T}d_{k}$=0

```julia
#verificar se a tupla está funcionando corretamente
function gradient_descent(f::Function,x::Vector,linear_method::Function;ϵ=1e-9)
    while(norm(∇(f,x))> ϵ)
      d = -∇(f,x)
      x = linear_search(f,x,d,linear_method)[2]
    end
    return (f(x),x)
end

```

    gradient_descent (generic function with 1 method)

```julia
gradient_descent(x->0.5*(x[1]-2)^2 + (x[2] - 1)^2 ,[0,1],armijo)
```

    (0.0, [2.0, 1.0])

# Metodo de Newton

O método de Newton serve para obter um $x^*\in\mathbb{R}^{n}$, tal que

\begin{equation}
\phi(x^*)=0
\end{equation}

onde $\phi:\mathbb{R}^n\rightarrow\mathbb{R}^n$ diferenciável. Portanto o método vai gerar uma sequência de pontos a partir de um $x_{0}$ que irá convergir para $x^*$.

Para construir a sequência vamos usar a série de Taylor.

\

\begin{equation}
\phi(x)=\phi(x_{k})+\nabla\phi(x_{k})(x-x_{k})=0
\end{equation}

\

Isolando $x$, obtemos

\

\begin{equation}
  x_{k+1}=x_{k}-(\nabla\phi(x_{k}))^{-1}\phi(x_{k})
\end{equation}

\

Agora iremos contextualizar o método de Newton para o nosso problema. Queremos achar um $x^*\in\mathbb{R}^{n}$, na qual $\nabla f(x^*)=0$. Assim, podemos considerar $\phi(x)=\nabla f(x)$, pois $\nabla f(x):\mathbb{R}^n\rightarrow\mathbb{R}^n$, logo ficaremos com a nossa sequência

\

\begin{equation}
  x_{k+1}=x_{k}-(\nabla^2f(x_{k}))^{-1}\nabla f(x_{k})
\end{equation}

\

Podemos considerar como método de descida com $t_{k}=1$ constante e $d_{k}=-(\nabla^2f(x_{k}))^{-1}\nabla f(x_{k})$.

Podemos ver na próxima imagem como o método funciona, com $\phi:\mathbb{R}\rightarrow\mathbb{R}$.

O próximo teorema fala que vai existir uma vizinhança do minimizador local $x^*$ de $f$ com $\nabla^2f(x^*)>0$, e se nosso $x_{0}$ pertencer a essa vizinhança, então o algoritmo do método de Newton com $t_{k}=1$ constante, gera uma sequência $\{x_{k}\}$ que converge superlinearmente para $x^*$, e $\nabla^2f(x_{k})>0$, $\forall k\in\mathbb{N}$, com convergência quadrática se $\nabla^2f$ for Lipchitz.

\

Teorema

Seja $f:\mathbb{R}^n\rightarrow\mathbb{R}$ de classe $c^2$. Suponha que $x^*\in\mathbb{R}^n$ seja um minimizador local de $f$, com $\nabla^2f(x^*)>0$. Então existe $\delta>0$ tal que se $x^{0}\in B(x^{*},\delta$), o algoritmo do método de Newton, com $t_{k}=1$ constante, gera uma sequência $(x_{k})$ tal que.

1. $\nabla^2f(x_{k})>0$, $\forall k\in\mathbb{N}$;
2. $(x_{k})$ converge superlinearmente para $x^*$;
3. Se $\nabla^2f$ é Lipschitz, então a convergência é quadrática;

Porem podemos tambem variar o passo $t_{k}$ e com isso temos o próximo teorema.

\

Teorema

Suponha $\nabla^2 f(x)>0$, $\forall x\in \mathbb{R}^{n}$. Então o algoritmo do Método de Newton, com $t_{k}$ calculado pela busca exata ou pela busca de Armijo, é globalmente convergente.

```julia
function newton_search(f::Function,x::Vector,linear_method::Function;ϵ=1e-9)
    while(norm(∇(f,x))> ϵ)
        d = -inv(Hessian(f,x))*∇(f,x)
        x = linear_search(f,x,d,linear_method)[2]
    end
    return (f(x),x)
end
```

    newton_search (generic function with 1 method)

```julia
newton_search(x->0.5*(x[1]-2)^2 + (x[2] - 1)^2 ,[0,1],armijo)
```

    (0.0, [2.0, 1.0])

#Metodo de quase-newton BFGS

O método de quase-Newton BFGS tem como principal ponto determinar a inversa da matriz Hessiana de maneira aproximada. Portanto, o que teriamos como direção $d_{k}=-(\nabla^2f(x_{k}))^{-1}\nabla f(x_{k})$ no método de Newton, vamos ter $d_{k}=-H_{k}\nabla f(x_{k})$ no método de quase-Newton, onde $H_{k}\in\mathbb{R}^{n\times n}$ é a matriz inversa da Hessiana aproximada, que é definida positiva e simétrica para todo $k$.

Podemos fazer as aproximações colocando uma condição.

\

\begin{equation}
H_{k+1}q_{k}=p_{k}
\end{equation}

\

Onde $q_{k}=\nabla f(x_{k+1})-\nabla f(x_{k})$ e $p_{k}=x_{k+1}-x_{k}$.

\

O método BFGS obtem a matriz aproximada $H_{k+1}$, onde para todo $k$, $H_{k+1}$ vai ser definida como

\

\begin{equation}
  H_{k+1}=H_{k}+(1+\frac{(q_{k})^{\top}H_{k}q_{k}}{(p_{k})^{\top}q_{k}})\frac{p_{k}(p_{k})^{\top}}{(p_{k})^{\top}q_{k}}-\frac{p_{k}(q_{k})^{\top}H_{k}+H_{k}q_{k}(p_{k})^{\top}}{(p_{k})^{\top}q_{k}}
\end{equation}

sendo $H_{k}=B_{k}^{-1}$

```julia
function quasi_newton_BFGS(f::Function,x::Vector,linear_method::Function;ϵ=1e-9)
    H = I
     flag = true
    g_new=∇(f,x)
    while(norm(g_new)> ϵ)
       d = -H*g_new
       x_old=x
       g_old = g_new
      x=linear_search(f,x,d,linear_method)[2]
      p = x - x_old
      g_new = ∇(f,x)
      q = g_new - g_old
      qT = transpose(q)
      pT = transpose(p)
       if(flag)
          H = ((qT*p)/(qT*q))*I #scaling of I
          flag =false
      end
      H = H+ (1+ ((qT*H*q)/(pT*q)))*((p*pT)/(pT*q)) - ((p*qT*H + H*q*pT)/(pT*q))
    end
    return (f(x),x)
end
```

    quasi_newton_BFGS (generic function with 1 method)

```julia
quasi_newton_BFGS(x->0.5*(x[1]-2)^2 + (x[2] - 1)^2 ,[0,1],armijo)
```

    (0.0, [2.0, 1.0])

# Metodo Gradiente Conjugada PR

O método do gradiente conjugado visa alcançar uma convergência mais rápida que o método do gradiente, e ter um custo computacional menor que o método de Newton.

Considere a função  $f:\mathbb{R}^n\rightarrow\mathbb{R}$ dada por.

\

\begin{equation}
f(\textbf{x})=\frac{1}{2}\textbf{x}^{\top}A\textbf{x}+\textbf{b}^{\top}\textbf{x}+c
\end{equation}

\

Com $A\in M_{n\times n}(\mathbb{R})$ e positiva, $\textbf{b}\in \mathbb{R}^n$ e $c\in \mathbb{R}$.

Onde a função terá apenas um minimizador $x^*$ global e que satisfaz.

\

\begin{equation}
A\textbf{x}^{*}+\textbf{b}=\nabla f(x^*)=0
\end{equation}

\

Vamos tomar como direções os vetores $A$-conjugados para o método de descida.

\

Definição

Seja $A\in M_{n\times n}(\mathbb{R})$ definida positiva. Dizemos que os vetores $\textbf{d}_{0},...,\textbf{d}_{k}\in \mathbb{R^{n}}$ são $A$-conjugados se
\begin{equation}
(\textbf{d}_{i})^{\top}A\textbf{d}_{j}=0
\end{equation}
para todos $i,j=1,..,k$, com $i\neq j$

\

Lema

Seja $A\in M_{n\times n}(\mathbb{R})$ definida positiva. Um conjunto qualquer de vetore $A$-conjugados é $\textbf{L.I}$.

\

Portanto pelo lema poderemos ter $n$ direções a serem tomadas $\textbf{d}_{0},...,\textbf{d}_{n-1}$.

\

\begin{equation}
\textbf{x}_{k+1}=\textbf{x}_{k}+t_{k}\textbf{d}_{k}
\end{equation}

\

sendo $t_{k}$ o minimizador de $f(\textbf{x}_{k}+t\textbf{d}_{k})$. Portanto a derivada na direção $\textbf{d}_{k}$ no próximo ponto $\textbf{x}_{k+1}$ é nula.
\begin{equation}
\nabla f(\textbf{x}_{k+1})^{\top}\textbf{d}=0
\end{equation}
como
\begin{equation}
\nabla f(\textbf{x}_{k+1})=A(\textbf{x}_{k}+t_{k}\textbf{d}_{k})+\textbf{b}=\nabla f(\text{x}_{k})+t_{k}A\textbf{d}_{k}
\end{equation}
Substituindo vamos obter $t_{k}$
 \begin{equation}
 t_{k}=-\frac{\nabla f(\textbf{x}_{k})^{\top}\textbf{d}_{k}}{(\textbf{d}_{k})^{\top}A\textbf{d}_{k}}
 \end{equation}

\

Vamos agora obter os $n$ vetores $A$-conjugados a partir de um $x_{0}$ dado. Defina $\textbf{d}_{0}=-\nabla f(\text{x}_{0})$, e para $k=0,...,n-2$
\begin{equation}
\textbf{d}_{k+1}=-\nabla f(\textbf{x}_{k+1})+\beta_{k}\textbf{d}_{k}
\end{equation}
Pela definição temos que
\begin{equation}
(\textbf{d})^{\top}_{k}A(-\nabla f(\textbf{x}_{k+1})+\beta_{k}\textbf{d}_{k})=0
\end{equation}
assim $\beta_{k}$ é dado por
\begin{equation}
\beta_{k}=\frac{(\textbf{d})^{\top}_{k}A\nabla f(\textbf{x}_{k+1})}{(\textbf{d})^{\top}_{k}A\textbf{d}_{k}}
\end{equation}
O próximo teorema nos garante que o método chegará ao mínimo de $f$.

\

Teorema

Considere a função anterior e seu minimizador $x^{*}$, definido $A\textbf{x}^{*}+\textbf{b}=\nabla f(\textbf{x}^{*})=0$. Dado $x_{0}\in \mathbb{R}^{n}$, a sequência finita definida por $\textbf{x}_{k+1}=\textbf{x}_{k}+t_{k}\textbf{d}_{k}$ cumpre $\textbf{x}_{n}=\textbf{x}^{*}$

\

Podemos fazer uma modificação para que o método economize no custo computacional no cálculo da hessiana, usando somente informações de primeira ordem. Essa variação do método funcionaria para qualquer função diferenciável, e tal modificação é dada por Polak e Ribiére como:
\begin{equation}
  \beta_{k-1}=\frac{<\nabla f(\textbf{x}_{k+1}), (\nabla f(\textbf{x}_{k+1})-\nabla f(\textbf{x}_{k}))>}{<\nabla f(\textbf{x}_{k}),\nabla f(\textbf{x}_{k})>}
\end{equation}

```julia
function gradient_conjugate_PR(f::Function,x::Vector,linear_method::Function;ϵ=1e-9,n_iter=100)
    k = 0
    n = length(x)
    g_new=∇(f,x)
    d = -g_new
    while(norm(g_new)> ϵ&&k<n_iter)
        x_old = x
        g_old = g_new
        x=linear_search(f,x,d,linear_method)[2]
        g_new  = ∇(f,x)
        β = max((transpose(g_new)*(g_new - g_old))/(transpose(g_old)*g_old),0)
        d = -g_new + β*d
        k = k + 1
    end
    return (f(x),x)
end
```

    gradient_conjugate_PR (generic function with 1 method)

```julia
gradient_conjugate_PR(x->0.5*(x[1]-2)^2 + (x[2] - 1)^2 ,[0,1],newton)
```

    (0.0, [2.0, 1.0])

# Modo de Usar

   Sejam os dados de entrada $x \in \mathcal{R}^n e f: \mathcal{R}^n \to \mathcal{R}$. Onde $x$ representa as variáveis de decisão e $f$ a função objetivo. Vamos mostrar como chamar os métodos de otimização irrestrita(no caso temos as funções: *gradient_descent, newton_search, gradient_conjugate_PR e quasi_newton_BFGS*) para resolver o problema de encontrar um ponto ótimo e  valor da função neste ponto.

   Considere o seguinte exemplo, seja $f(x_1,x_2)= \frac{1}{2}(x_1 - 2)^2 + (x_2 - 1)^2$, $x = \begin{pmatrix}1 \\0\end{pmatrix}$ e $d  = \begin{pmatrix}3 \\1\end{pmatrix}$. Esses podem ser passados para um método da seguinte forma:

```julia
gradient_descent(x->0.5*(x[1]-2)^2 + (x[2] - 1)^2 ,[1,0],secao_aurea)
```

    (9.129599700888681e-20, [1.9999999995972084, 1.0000000001008735])

   Onde o primeiro campo da função implementa uma função anônima para representar $f(x)$, com formato (vetores)$\to$ (função expressa). Devemos utilizar de operações aritméticas padrões sobre um vetor[$i$], que refere-se a coordenada $i$ do vetor(em julia começa de 1 até n), para expressar $f(x)$. O campo seguinte dp método faz referência à $x$ implementado como um Array'([$x_1$,$x_2$].

  No último campo temos uma função referenciando o método de busca linear(no caso seção aurea) usado, temos três referências implementadas secao_aurea, armijo e newton. Elas podem ser chamadas da mesma forma do exemplo anterior, que utiliza parâmetros definidos pelo livro-texto da disciplina, ou podemos utilizar funções anonimas para chamar um método customizado da forma que foi definida no [Projeto 1](https://colab.research.google.com/drive/1Nt6ffNGr-3ej0gCgIW2sG5DYSnJ7Pywh?usp=sharing). Veja que agora nesse projeto além dos parâmetros adicionais para os métodos de busca linear, o mesmo ocorre para os métodos de otimização irrestrita.

```julia
gradient_descent(x->0.5*(x[1]-2)^2 + (x[2] - 1)^2 ,[1,0],(f,x,d)->armijo(f,x,d,t=0.7,η = 0.5),ϵ=1e-4)
```

    (1.8288426094768024e-9, [1.999946882144, 0.999979552768])

 Na proxima seção discutimos os parâmetros para cada método. A saída do método  retorna uma tupla da forma ($f(x^*),x^*$), ou seja, o valor da função objetivo no melhor ponto encontrado e o ponto.

# Parâmetros dos Métodos

   Aqui colocaremos as funções dos métodos implementados e a descrição sobre seus parâmetros. Parametros depois do caracter ";" são chamados de argumentos palavra-chave. Esses devem vir depois dos argumentos posicionais e da forma (palavra-chave) = valor. Algumas palavras-chave estão em unicode para acessar estas no julia basta escrever da mesma forma que em latex(ex. \epsilon) e pressionar a tecla 'tab', ou seja , $\epsilon$ = \epsilona + tab. No ambiente Colab para inserir os caracteres basta copiar das funções, pois o mesmo não possui suporte para inserir caracteres unicode.

gradient_descent(f::Function,x::Vector,linear_method::Function;ϵ=1e-9)
>  Algoritmo de Gradiente Descendente clássico.

*  ϵ(\epsilon) -  tolerância utilizada para verificar convergencia. Com menor precisão mais rápido converge.

---

newton_search(f::Function,x::Vector,linear_method::Function;ϵ=1e-9)
> Método de Newton clássico tal que não utiliza aproximação da inversa da hessiana. No caso, tentamos utilizar as bibliotecas aqui expostas para calcular com precisão.

*  ϵ(\epsilon) -  tolerância utilizada para verificar convergencia. Com menor precisão mais rápido converge.

---
quasi_newton_BFGS(f::Function,x::Vector,linear_method::Function;ϵ=1e-9)

>  Algoritmo de Quase Newton usando método de Broyden, Fletcher, Goldfarb e Shanno(BFGS) para calcular uma aproximação da inversa da hessiana. Utiliza tecnica de escalonamento da matriz hessiana inicial como um fator multiplicativo da identidade.

*  ϵ(\epsilon) -  tolerância utilizada para verificar convergencia. Com menor precisão mais rápido converge.

---
gradient_conjugate_PR(f::Function,x::Vector,linear_method::Function;ϵ=1e-9,n_iter=100)
>  Algoritmo de Gradiente Conjugado usando método de Polak e Ribiére para calcular direções conjugadas. Utiliza tecnica de reinicialização com o método da gradiente caso $\beta_k$ sejá negativo. Como a função de entrada não é necessariamente quadratica limitamos o número de iterações.

*  ϵ(\epsilon) -  tolerância utilizada para verificar convergencia. Com menor precisão mais rápido converge.
* n_iter - número máximo de iterações que o método pode executar.

---

Todos os métodos retornam a função objetivo e o melhor ponto encontrado. Vale ressaltar que podemos passar em linear_method uma função de passo constante ou funções com passos customizados aos métodos.

#Testes

Nesta seção definimos as instâncias de testes que usamos e quais os resultados os métodos encontraram.

Instâncias

   As instâncias dadas são definidas em termo de função objetivo, valor inicial de $x$ e direção de descida $d$.  Abaixo apresentamos uma lista com os 5 valores entrada usados.

1.   instância 1
   *   $f_1(x)=(x_1 -2)^4 + (x_1 - 2x_2)^2$
   *   $x_0 = \begin{pmatrix}0 \\ 3\end{pmatrix}$
   *   $d_0 = \begin{pmatrix}-\nabla f_1(x_0)\end{pmatrix}$
2.   instância 2
   *   $f_2(x)=100(x_2 -x_1^2)^2 + (1 - x_1)^2$
   *   $x_0 = \begin{pmatrix}-1,9\\2\end{pmatrix}$
   *   $d_0 = \begin{pmatrix}-\nabla f_2(x_0)\end{pmatrix}$
3.   instância 3
   *   $f_3(x)=1,5 - x_1(1-x_2)^2 + (2,25 - x_1(1-x_2^2)^2+(2,625-x_1(1-x_2^3))^2$
   *   $x_0 = \begin{pmatrix}0\\0\end{pmatrix}$
   *   $d_0 = \begin{pmatrix}-\nabla f_3(x_0)\end{pmatrix}$
4.   instância 4
   *   $f_4(x)=(x_1 - 2x_2+5x_2^2 - x_2^3 -13)^2 + (x_1 - 14x_2 + x_2^2 + x_2^3 -29)^2$
   *   $x_0 = \begin{pmatrix}15\\-2\end{pmatrix}$
   *   $d_0 = \begin{pmatrix}-\nabla f_4(x_0)\end{pmatrix}$
5.   instância 5
   *   $f_5(x)=100(x_3 - {(\frac{x_1+x_2}{2})}^2)^2 + (1-x_1)^2 + (1-x_2)^2$
   *   $x_0 = \begin{pmatrix}-1,2\\2\\0\end{pmatrix}$
   *   $d_0 = \begin{pmatrix}-\nabla f_5(x_0)\end{pmatrix}$

```julia
#funções objetivo
f1 = x-> (x[1]-2)^4 + (x[1] - 2*x[2])^2
f2 = x-> 100*(x[2]-(x[1])^2)^2 + (1 - x[1])^2
f3 = x-> 1.5 - x[1]*(1-x[2])^2 + (2.25 - x[1]*(1-(x[2])^2))^2 +(2.625 - x[1]*(1-x[2]^3))^2
f4 = x-> (x[1] - 2*x[2]+5*x[2]^2 - x[2]^3 -13)^2 + (x[1] - 14*x[2] + x[2]^2 + x[2]^3 -29)^2
f5 = x-> 100*(x[3] - ((x[1]+x[2])/2)^2)^2 + (1-x[1])^2 + (1-x[2])^2

#valores de x indexado por função
x0 = [[0,3],[-1.9,2],[0,0],[15,-2],[-1.2,2,0]]

```

    5-element Array{Array{Float64,1},1}:
     [0.0, 3.0]
     [-1.9, 2.0]
     [0.0, 0.0]
     [15.0, -2.0]
     [-1.2, 2.0, 0.0]

##Resultados

Abaixo mostramos o resultados usando os algoritmos do livro-texto com os valores indicados.

```julia
functions = [f1,f2,f3,f4,f5]
linear_methods = [secao_aurea,newton,armijo]
methods = [gradient_descent,newton_search,gradient_conjugate_PR,quasi_newton_BFGS]
linear_methods_names = ["Seção Aurea","Método de Newton","Condição de Armijo"]
methods_names = ["Método da Gradiente","Método de Newton","Método do Gradiente Conjugado","Método de Quase Newton"]
println("----------------------------------------------------------------")
for i = 1:5
    println("instancia ",i,":")
    println("----------------------------------------------------------------")
    for j = 1:3
    print("Método de Busca Linear Usado: ")
       println(linear_methods_names[j])
       println("----------------------------------------------------------------")
       for k = 1:4
         println(methods_names[k])
         (best_obj,best_point) = methods[k](functions[i],x0[i],linear_methods[j],ϵ=1e-4)
         print("Melhor solução : ")
         println(best_point)
         print("Valor de f na solução : ")
         println(best_obj)
         println("----------------------------------------------------------------")
       end
    end
end
```

    ----------------------------------------------------------------
    instancia 1:
    ----------------------------------------------------------------
    Método de Busca Linear Usado: Seção Aurea
    ----------------------------------------------------------------
    Método da Gradiente
    Melhor solução : [2.0283997404430587, 1.0142108251982414]
    Valor de f na solução : 6.50995298024256e-7
    ----------------------------------------------------------------
    Método de Newton
    Melhor solução : [1.9945816244848118, 0.9973003366637665]
    Valor de f na solução : 1.2247971485622868e-9
    ----------------------------------------------------------------
    Método do Gradiente Conjugado
    Melhor solução : [2.0081473404962593, 1.004066056213807]
    Valor de f na solução : 4.6380865801980474e-9
    ----------------------------------------------------------------
    Método de Quase Newton
    Melhor solução : [2.0081473470237348, 1.0040660594522226]
    Valor de f na solução : 4.63810224323956e-9
    ----------------------------------------------------------------
    Método de Busca Linear Usado: Método de Newton
    ----------------------------------------------------------------
    Método da Gradiente
    Melhor solução : [2.028693224146293, 1.014357243140677]
    Valor de f na solução : 6.782767992586427e-7
    ----------------------------------------------------------------
    Método de Newton
    Melhor solução : [1.9944914094830326, 0.9972544784053405]
    Valor de f na solução : 1.228701604749209e-9
    ----------------------------------------------------------------
    Método do Gradiente Conjugado
    Melhor solução : [2.008189390796104, 1.0040870114891156]
    Valor de f na solução : 4.734034494159106e-9
    ----------------------------------------------------------------
    Método de Quase Newton
    Melhor solução : [2.0081546651794593, 1.004069709948672]
    Valor de f na solução : 4.6544776659875954e-9
    ----------------------------------------------------------------
    Método de Busca Linear Usado: Condição de Armijo
    ----------------------------------------------------------------
    Método da Gradiente
    Melhor solução : [2.02665188360004, 1.0133378796026655]
    Valor de f na solução : 5.051286659827713e-7
    ----------------------------------------------------------------
    Método de Newton
    Melhor solução : [1.9768779601122228, 0.9884389800561114]
    Valor de f na solução : 2.8582787741446877e-7
    ----------------------------------------------------------------
    Método do Gradiente Conjugado
    Melhor solução : [1.9763847026589918, 0.9881978812130919]
    Valor de f na solução : 3.1113183105436573e-7
    ----------------------------------------------------------------
    Método de Quase Newton
    Melhor solução : [2.0137136323648757, 1.0068603014888817]
    Valor de f na solução : 3.541654945558272e-8
    ----------------------------------------------------------------
    instancia 2:
    ----------------------------------------------------------------
    Método de Busca Linear Usado: Seção Aurea
    ----------------------------------------------------------------
    Método da Gradiente
    Melhor solução : [1.0000841336750939, 1.0001687580792034]
    Valor de f na solução : 7.101867069310204e-9
    ----------------------------------------------------------------
    Método de Newton
    Melhor solução : [0.999999965523016, 0.9999998709159951]
    Valor de f na solução : 3.62750810136467e-13
    ----------------------------------------------------------------
    Método do Gradiente Conjugado
    Melhor solução : [1.0000000111671055, 1.000000022389576]
    Valor de f na solução : 1.2501077078382175e-16
    ----------------------------------------------------------------
    Método de Quase Newton
    Melhor solução : [0.9999998415809243, 0.9999996882947783]
    Valor de f na solução : 2.7731274582003034e-14
    ----------------------------------------------------------------
    Método de Busca Linear Usado: Método de Newton
    ----------------------------------------------------------------
    Método da Gradiente
    Melhor solução : [1.0000982230209643, 1.0001969545557612]
    Valor de f na solução : 9.672648582999265e-9
    ----------------------------------------------------------------
    Método de Newton
    Melhor solução : [1.0000000635709596, 1.0000000664518338]
    Valor de f na solução : 3.723699620247635e-13
    ----------------------------------------------------------------
    Método do Gradiente Conjugado
    Melhor solução : [0.9999999466633526, 0.9999998930625945]
    Valor de f na solução : 2.8517735538419663e-15
    ----------------------------------------------------------------
    Método de Quase Newton
    Melhor solução : [0.9999999016722615, 0.999999825896025]
    Valor de f na solução : 6.052532483692413e-14
    ----------------------------------------------------------------
    Método de Busca Linear Usado: Condição de Armijo
    ----------------------------------------------------------------
    Método da Gradiente
    Melhor solução : [1.0000707864488372, 1.0001416895995703]
    Valor de f na solução : 5.011968830825379e-9
    ----------------------------------------------------------------
    Método de Newton
    Melhor solução : [0.9999863288413037, 0.9999725899968034]
    Valor de f na solução : 1.8736125050268438e-10
    ----------------------------------------------------------------
    Método do Gradiente Conjugado
    Melhor solução : [0.9999613928597925, 0.9999228312671108]
    Valor de f na solução : 1.4907053770578261e-9
    ----------------------------------------------------------------
    Método de Quase Newton
    Melhor solução : [0.9999998770742431, 0.9999997442198411]
    Valor de f na solução : 2.4968571102062078e-14
    ----------------------------------------------------------------
    instancia 3:
    ----------------------------------------------------------------
    Método de Busca Linear Usado: Seção Aurea
    ----------------------------------------------------------------
    Método da Gradiente
    Melhor solução : [3.128735223443592, -0.6024183462061982]
    Valor de f na solução : -5.057151960624211
    ----------------------------------------------------------------
    Método de Newton
    Melhor solução : [1.8698891206445696e-23, 1.0000000257427923]
    Valor de f na solução : 13.453125
    ----------------------------------------------------------------
    Método do Gradiente Conjugado
    Melhor solução : [3.128742628614038, -0.6024180239525193]
    Valor de f na solução : -5.057151960582296
    ----------------------------------------------------------------
    Método de Quase Newton
    Melhor solução : [3.1287373670153893, -0.6024182475460674]
    Valor de f na solução : -5.057151960632495
    ----------------------------------------------------------------
    Método de Busca Linear Usado: Método de Newton
    ----------------------------------------------------------------
    Método da Gradiente
    Melhor solução : [3.1287352028209146, -0.6024184254870121]
    Valor de f na solução : -5.0571519606238295
    ----------------------------------------------------------------
    Método de Newton
    Melhor solução : [0.0, 1.0000000257427324]
    Valor de f na solução : 13.453125
    ----------------------------------------------------------------
    Método do Gradiente Conjugado
    Melhor solução : [6.773108601416899e-8, -338.18991582501934]
    Valor de f na solução : 6.5896537349574755
    ----------------------------------------------------------------
    Método de Quase Newton
    Melhor solução : [3.1287373657964417, -0.6024182480536967]
    Valor de f na solução : -5.057151960632494
    ----------------------------------------------------------------
    Método de Busca Linear Usado: Condição de Armijo
    ----------------------------------------------------------------
    Método da Gradiente
    Melhor solução : [3.128752764345317, -0.6024166153841736]
    Valor de f na solução : -5.05715196017133
    ----------------------------------------------------------------
    Método de Newton
    Melhor solução : [0.0, 1.0000000257427324]
    Valor de f na solução : 13.453125
    ----------------------------------------------------------------
    Método do Gradiente Conjugado
    Melhor solução : [3.1287299140256137, -0.6024191478137798]
    Valor de f na solução : -5.057151960520517
    ----------------------------------------------------------------
    Método de Quase Newton
    Melhor solução : [3.128737505427481, -0.6024177945713456]
    Valor de f na solução : -5.057151960624901
    ----------------------------------------------------------------
    instancia 4:
    ----------------------------------------------------------------
    Método de Busca Linear Usado: Seção Aurea
    ----------------------------------------------------------------
    Método da Gradiente
    Melhor solução : [11.412859591473364, -0.896800464571201]
    Valor de f na solução : 48.98425368191556
    ----------------------------------------------------------------
    Método de Newton
    Melhor solução : [11.412778924598863, -0.8968052543634565]
    Valor de f na solução : 48.98425367924002
    ----------------------------------------------------------------
    Método do Gradiente Conjugado
    Melhor solução : [11.412783875317597, -0.8968050657933547]
    Valor de f na solução : 48.984253679254536
    ----------------------------------------------------------------
    Método de Quase Newton
    Melhor solução : [11.412783515134125, -0.8968050838773837]
    Valor de f na solução : 48.984253679252916
    ----------------------------------------------------------------
    Método de Busca Linear Usado: Método de Newton
    ----------------------------------------------------------------
    Método da Gradiente
    Melhor solução : [5.000028031565052, 3.9999995195985636]
    Valor de f na solução : 1.1398716032486536e-9
    ----------------------------------------------------------------
    Método de Newton
    Melhor solução : [11.412778897198613, -0.8968052611327226]
    Valor de f na solução : 48.98425367924001
    ----------------------------------------------------------------
    Método do Gradiente Conjugado
    Melhor solução : [4.999999999755535, 3.999999999997393]
    Valor de f na solução : 1.7298188579764838e-19
    ----------------------------------------------------------------
    Método de Quase Newton
    Melhor solução : [4.999999678405315, 4.000000006064546]
    Valor de f na solução : 1.5058096376544588e-13
    ----------------------------------------------------------------
    Método de Busca Linear Usado: Condição de Armijo
    ----------------------------------------------------------------
    Método da Gradiente
    Melhor solução : [11.412859482990637, -0.8968003901403515]
    Valor de f na solução : 48.98425368191176
    ----------------------------------------------------------------
    Método de Newton
    Melhor solução : [11.412778905444904, -0.8968052617125944]
    Valor de f na solução : 48.984253679240055
    ----------------------------------------------------------------
    Método do Gradiente Conjugado
    Melhor solução : [4.999991882872304, 4.000000120167337]
    Valor de f na solução : 9.626556535492628e-11
    ----------------------------------------------------------------
    Método de Quase Newton
    Melhor solução : [11.412779309973189, -0.8968052250663022]
    Valor de f na solução : 48.984253679240126
    ----------------------------------------------------------------
    instancia 5:
    ----------------------------------------------------------------
    Método de Busca Linear Usado: Seção Aurea
    ----------------------------------------------------------------
    Método da Gradiente
    Melhor solução : [1.0000559720951359, 1.0000559720950326, 1.0001122085832441]
    Valor de f na solução : 6.2725765454663074e-9
    ----------------------------------------------------------------
    Método de Newton
    Melhor solução : [1.0000000931720026, 1.0000001697254244, 1.0000002629387559]
    Valor de f na solução : 3.74879124070978e-14
    ----------------------------------------------------------------
    Método do Gradiente Conjugado
    Melhor solução : [1.0000007027232793, 0.9999991712098528, 0.999999876134996]
    Valor de f na solução : 1.1811979340525262e-12
    ----------------------------------------------------------------
    Método de Quase Newton
    Melhor solução : [1.0000056324685829, 0.9999796501854512, 0.999985255443021]
    Valor de f na solução : 4.459139934193211e-10
    ----------------------------------------------------------------
    Método de Busca Linear Usado: Método de Newton
    ----------------------------------------------------------------
    Método da Gradiente
    Melhor solução : [0.999949279391135, 0.9999492793902233, 0.9998980627129006]
    Valor de f na solução : 5.170024708230077e-9
    ----------------------------------------------------------------
    Método de Newton
    Melhor solução : [1.000000142656286, 1.000000142656286, 1.0000002813352955]
    Valor de f na solução : 4.228352072614272e-14
    ----------------------------------------------------------------
    Método do Gradiente Conjugado
    Melhor solução : [0.9999353675877346, 0.9999614096421303, 0.9998961480613571]
    Valor de f na solução : 5.706485634094193e-9
    ----------------------------------------------------------------
    Método de Quase Newton
    Melhor solução : [1.000005632216855, 0.9999796890372052, 0.99998529527967]
    Valor de f na solução : 4.4432482336449995e-10
    ----------------------------------------------------------------
    Método de Busca Linear Usado: Condição de Armijo
    ----------------------------------------------------------------
    Método da Gradiente
    Melhor solução : [0.9999606198096178, 0.9999606198149855, 0.9999211908486427]
    Valor de f na solução : 3.101851644586935e-9
    ----------------------------------------------------------------
    Método de Newton
    Melhor solução : [0.9999999612690533, 0.9999999612690533, 0.9999999221892637]
    Valor de f na solução : 3.01234170936249e-15
    ----------------------------------------------------------------
    Método do Gradiente Conjugado
    Melhor solução : [1.000218907418259, 1.0000617897656818, 1.000284307708624]
    Valor de f na solução : 5.302783673464355e-8
    ----------------------------------------------------------------
    Método de Quase Newton
    Melhor solução : [1.0000011920211644, 1.0000012234280915, 1.0000025647959099]
    Valor de f na solução : 5.148089494750321e-12
    ----------------------------------------------------------------

Podemos ver que conseguimos convergir para ótimos globais na maioria das instâncias. Porém veja que em instâncias onde temos fatores não quadráticos, o algoritmo de newton e gradiente conjugado podem não convergir para um minimo global. Outro fator que pode interferir são os métodos de busca linear, como pode ser visto, na instância 4 o método da seção áurea gerou um mínimo local que é pior para todos os algoritmos. Fato dado que o método da seção áurea possui dependência por intervalos unimodais.

Para melhorar a convergência podemos utilizar condições para busca linear mais fortes como a de Wolfe. E utilizar métodos modificados para lidar com carateristicas específicas de funções.

#Referências
 $[1]$ **Ademir A.R.** e **Elizabeth W.K.** (2011). *Um Curso De Otimização. Curitiba.*