```python
%%capture
## compile PyRoss for this notebook
import os
owd = os.getcwd()
os.chdir('../../')
%run setup.py install
os.chdir(owd)
%matplotlib inline
```

```python
import numpy as np
import matplotlib.pyplot as plt
import pyross
```

In this notebook we consider a control protocol consisting of a lockdown. For our numerical study we generate synthetic data using the deterministic SEkIkIkR model.

While we use the UK age structure and contact matrix, we emphasise that, with the exception of $\beta$, **the model parameters considered in this notebook have NOT been obtained from real data, but rather are chosen ad-hoc**.

**Outline of this notebook:**

1. We briefly summarize the SEkIkIkR model (still to do).
2. We load the age structure and contact matrix for UK. The contact matrix is generally given as
\begin{equation}
    C = C_{H} + C_{W} + C_{S} + C_{O},
\end{equation}
where the four terms denote the number of contacts at home, work, school, and all other remaining contacts.
3. We define the other model parameters of the SEkIkIkR model **(again, these are not fitted to any real data, but rather chosen ad-hoc)**.
4. We define a "lockdown-protocol":
    1. After a fixed time, a lockdown is imposed. The contact matrix is reduced to
    \begin{equation}
        C = C_{H} + 0.5\cdot C_W + 0.4 \cdot C_O,
        \end{equation}
        i.e. the "home" contacts $C_H$, and 50% of the "work" as well as 40% of the "other" contacts are reainted. The latter two model that at the beginning of lockdown, a significant amount of people still goes to work, and might be lenient in following the lockdown advice.
    2. 2 Days after the initial lockdown, the 40% "social interaction" part of the contact matrix is removed, the work matrix is reduced to 10%, so that we have
        \begin{equation}
        C = C_{H} + 0.1\cdot C_W.
        \end{equation}
        This models that people take the lockdown seriously and seize to interact socially.
5. We run a deterministic simulation of this lockdown protocol, and compare the results to the UK data.

## 2. Load UKage structure and contact matrix

```python
my_data = np.genfromtxt('../data/age_structures/UK.csv', delimiter=',', skip_header=1)
aM, aF = my_data[:, 1], my_data[:, 2]

Ni0=aM+aF;

M=16  ## number of age classes

Ni = Ni0[:M]
N=np.sum(Ni)

print("Age groups are in brackets of 5 (i.e. 0-4, 5-9, 10-14, .. , 75-79).")
print("Number of individuals in each bracket:")
print(Ni.astype('int'))
print("Total number of individuals: {0}".format(np.sum(Ni.astype('int'))))
```

    Age groups are in brackets of 5 (i.e. 0-4, 5-9, 10-14, .. , 75-79).
    Number of individuals in each bracket:
    [3951046 4114237 3884626 3684534 4120269 4510345 4683636 4519933 4270785
     4353894 4674918 4463447 3799485 3406990 3329496 2343961]
    Total number of individuals: 64111602

```python
# Get individual contact matrices
CH, CW, CS, CO = pyross.contactMatrix.UK()

# By default, home, work, school, and others contribute to the contact matrix
C = CH + CW + CS + CO

# Illustrate the individual contact matrices:
fig,aCF =  plt.subplots(2,2);
aCF[0][0].pcolor(CH, cmap=plt.cm.get_cmap('GnBu', 10));
aCF[0][1].pcolor(CW, cmap=plt.cm.get_cmap('GnBu', 10));
aCF[1][0].pcolor(CS, cmap=plt.cm.get_cmap('GnBu', 10));
aCF[1][1].pcolor(CO, cmap=plt.cm.get_cmap('GnBu', 10));
```

The above contact matrices illustrate the interactions at home (upper left), work (upper right), school (lower left), and the other remaining contacts (lower right). x- and y- axes denote the age groups, a darker color indicates more interaction.

## 3. Define model parameters

**Note: These have not been fitted to real data.**

```python
alpha=0.3              # fraction of symptomatics who self-isolate
beta = 0.05984224          # probability of infection on contact
gE   = 1/2.72            # recovery rate of exposeds
kI   = 10;               # # of stages of I class
kE   = 10;                # # of stages of E class
gIa  = 1./7              # recovery rate of  infectives
gIs  = 1./17.76
fsa = 0.5

# We start with one symptomatic infective in each of the age groups 6-13
S0 = np.zeros(M)
I0 = np.zeros((kI,M));
E0 = np.zeros((kE,M));
for i in range(kI):
    I0[i, 6:13]= 1
for i in range(M) :
    S0[i] = Ni[i] - np.sum(I0[:,i]) - np.sum(E0[:,i])
I0 = np.reshape(I0, kI*M)/kI;
E0 = np.reshape(E0, kE*M)/kE;
```

## 4. Define events for protocol

```python
# Dummy event for initial (standard) contact matrix
events = [lambda t: 1]
contactMatrices = [C]

T_offset = 19 # see fitParamBeta notebook where the beta parameter of the SEkIkIkR model is fitted
# After 20 days, start lockdown
lockdown_threshold_0 = T_offset+19
def event0(t,rp):
    return t - lockdown_threshold_0
events.append(event0)
contactMatrices.append( CH + 0.5*CW + .4*CO ) # for a short time,
                                              # people still have some social contacts

# After 2 days, decrease contacts further
lockdown_threshold_1 = T_offset+21
def event1(t,rp):
    return t- lockdown_threshold_1
events.append(event1)
contactMatrices.append( CH + 0.2*CW)

'''
# After 70 days, add 50% of school contacts to contact matrix
lockdown_threshold_2 = T_offset+260
def event2(t,rp):
    return t - lockdown_threshold_2
events.append(event2)
contactMatrices.append( CH + 0.2*CW + 0.5*CS ) # everybody in lockdown
''';
```

## 5. Simulate protocol and analyse results

#### Initialise pyross.control, run and plot a single test simulation

```python
# duration of simulation
Tf=500;  Nf=Tf+1;

# intantiate model
parameters = {'beta':beta, 'gE':gE, 'gIa':gIa, 'gIs':gIs,
            'kI':kI, 'kE' : kE, 'fsa':fsa, 'alpha':alpha}
model = pyross.control.SEkIkIkR(parameters, M, Ni)

# run model once
data=model.simulate(S0, E0, 0*I0, I0,
                    events,contactMatrices, Tf, Nf)
```

```python
# Load data
my_data = np.genfromtxt('../data/covid-cases/uk.txt', delimiter='', skip_header=7)
day, cases = my_data[:,0], my_data[:,1]
```

```python
# Plot result

t = data['t'];
# to compare with the UK dataset, we need the total number of known cases.
# We assume that this is given by
# (All known cases) = (Total population) - (# of suspectibles) + (# of asymptomatics),
Ia = (model.Ia(data))
summedAgesIa = Ia.sum(axis=1)
S = (model.S(data))
summedAgesS = S.sum(axis=1)
trajectory = N - summedAgesS + summedAgesIa
index = T_offset # determined in notebook fitParamBeta, where beta was fitted to the data

lw=3
fig,ax = plt.subplots(1,1,figsize=(8,5))
ax.plot(t[index:]-t[index],trajectory[index:],lw=lw,
       label='SEkIkIkR model')
ax.axvline(data['events_occured'][0][0]-t[index],
           color='crimson',lw=lw,
          label='Beginning of lockdown',ls='--')
ax.plot(cases,marker='o',ls='',
       label='UK data')
#ax.set_xlim(0,Tf)
ax.set_xlim(0,40)
ax.set_ylim(0,5e4)
fs=20
ax.legend(loc='best',fontsize=15,framealpha=1)
ax.set_xlabel('time since first infection [days]',fontsize=fs)
ax.set_ylabel('Total number of cases',fontsize=fs)
plt.show()
plt.close()

fig,ax = plt.subplots(1,1,figsize=(8,5))
ax.plot(t[index:]-t[index],trajectory[index:],lw=lw,
       label='SEkIkIkR model')
ax.axvline(data['events_occured'][0][0]-t[index],
           color='crimson',lw=lw,
          label='Beginning of lockdown',ls='--')
ax.plot(cases,#marker='o',
        lw=lw,ls='--',
       label='UK data')
ax.set_xlim(0,100)
ax.set_ylim(0,5e5)
fs=20
ax.legend(loc='best',fontsize=fs,framealpha=1)
ax.set_xlabel('time since first infection [days]',fontsize=fs)
ax.set_ylabel('Total number of cases',fontsize=fs)
fig.savefig('ex7_SEkIkIkR_lockdown.pdf',bbox_inches='tight')
plt.show()
plt.close()

print("Number of symptomatic infectives (to get a feeling for 'active cases'):")
Is = (model.Is(data))
summedAgesIs = Is.sum(axis=1)

fig,ax = plt.subplots(1,1,figsize=(8,5))
ax.plot(t[index:]-t[index],summedAgesIs[index:],lw=lw,
       label='SEkIkIkR model')
ax.axvline(data['events_occured'][0][0]-t[index],
           color='crimson',lw=lw,
          label='Beginning of lockdown',ls='--')
fs=20
ax.legend(loc='best',fontsize=fs,framealpha=1)
ax.set_xlabel('time since first infection [days]',fontsize=fs)
ax.set_ylabel('Total number of cases',fontsize=fs)
plt.show()
plt.close()
```

(The reader is encouraged to play around with the contact matrices in the lockdown protocol. Even if after 20 days, only the home contact matrix remains, the curvature remains positive and the total number of active cases "explodes".)

**(We emphasise again that the model used here is, except for the parameter $\beta$, not fitted to the UK data!)**

```python

```

```python

```

examples/control/ex08 - SEkIkIkR - UK - lockdown.ipynb


 INF-285 - Computación Científica

 EDO - Aplicaciones Avanzadas

  [S]cientific [C]omputing [T]eam

 Version: 1.15

## Algunos algoritmos utilizados en este notebook han sido obtenidos del libro "Lloyd N. Trefethen, Spectral Methods in MATLAB, SIAM, Philadelphia, 2000" y traducidos a python.

# Tabla de Contenidos
* [Introducción](#intro)
* [Configuraciones](#config)
* [Problemas de Valor Inicial](#IVP)
    * [Algunos Solver Típicos](#solver)
        * [Regiones de Estabilidad](#stab)
        * [Convergencia](#conver)
    * [Problemas de segundo orden y más dimensiones](#high)
* [Problemas de Valor de Frontera](#BVP)
    * [Diferencias Finitas](#DD)
    * [Método de Disparo](#MD)
* [Acknowledgements](#acknowledgements)

# Introducción
En este notebook estudiaremos métodos numéricos para la resolución de ecuaciones diferenciales ordinarias (EDO). Se verán problemas de valor inicial (IVP) y de valor de frontera (BVP), sus principales métodos de resolución y se analizarán estabilidad y convergencia para los primeros mencionados.

# Configuraciones

### Librerías y parámetros utilizados en el notebook

```python
%matplotlib inline
import matplotlib.pyplot as plt
from matplotlib import pyplot #
import numpy as np
from scipy.integrate import odeint
from pylab import * #
from numpy import linalg as LA
from matplotlib.legend_handler import HandlerLine2D
from scipy.linalg import toeplitz
from scipy.optimize import root
from ipywidgets import interact
import sympy as sym
import matplotlib as mpl
mpl.rcParams['font.size'] = 14
mpl.rcParams['axes.labelsize'] = 20
mpl.rcParams['xtick.labelsize'] = 14
mpl.rcParams['ytick.labelsize'] = 14
sym.init_printing()

# Global parameter that controls the figure's size
L = 15

# Source: https://github.com/tclaudioe/Scientific-Computing/blob/master/SC5/02%20Differentiation%20matrices.ipynb
def plot_matrices_with_values(ax,M):
    N=M.shape[0]
    cmap = plt.get_cmap('GnBu')
    ax.matshow(M, cmap=cmap)
    for i in np.arange(0, N):
        for j in np.arange(0, N):
            ax.text(i, j, '{:.2f}'.format(M[i,j]), va='center', ha='center', color='r')
```

# Problemas de Valor Inicial

Un problema de valor inicial (IVP) corresponde a una ecuación diferencial ordinaria de la forma:

$$ \dot{y}(t) = f(t,y(t)) $$
sujeta a una condición inicial:

$$ y(t_0) = y_0 $$
siendo un sistema que evoluciona en el tiempo.

# Algunos Solver Típicos

Los solver típicos para resolver IVP's son los denominados métodos de Runge-Kutta, los cuales son un conjunto de métodos iterativos para la resolución numérica de ecuaciones diferenciales. Dentro de este conjunto, los más utilizados y los cuales veremos en este curso son:

* Runge-Kutta de primer orden (RK1) o Método de Euler:

\begin{align*}
k_1 &= h \cdot f(t_i,y_i) \\
y_{i+1} &= y_i + k_1
\end{align*}

* Runge-Kutta de segundo orden (RK2)

\begin{align*}
k_{1} &= h \cdot f(t_i,y_i) \\
k_{2} &= h \cdot f(t_i + h, y_i + k_1) \\
y_{i+1} &= y_i + \dfrac{1}{2}(k_1 + k_2) \\
\end{align*}

* Runge-Kutta de cuarto orden (RK4)

\begin{align*}
k_{1} &= h \cdot f(t_i,y_i) \\
k_{2} &= h \cdot f(t_i + \dfrac{h}{2}, y_i + \dfrac{1}{2}k_1) \\
k_{3} &= h \cdot f(t_i + \dfrac{h}{2}, y_i + \dfrac{1}{2}k_2) \\
k_{4} &= h \cdot f(t_i + h, y + k_3) \\
y_{i+1} &= y_i + \dfrac{1}{6}(k_1 + 2k_2 + 2k_3 + k_4)
\end{align*}

Para todos los métodos vistos, $h$ corresponde a $\Delta t$, es decir, la distancia entre un tiempo $t_i$ y el siguiente $t_{i+1}$, con $i =0,...,n-1$, al discretizar el intervalo de tiempo en $n$ subintervalos.

```python
# Forward Euler Method
def euler_ode(y,t,f,h):
    return y+h*f(t,y)

# Runge-Kutta of Second order
def RK2_ode(y,t,f,h):
    k1=y+h/2.0*f(t,y) #or euler_ode(y,t,f,h)
    return y+h*f(t+h/2.0,k1)
    # k1=h*f(t,y)
    # return y+h*f(t+h/2.0,y+k1/2.0)

# Runge-Kutta
def RK4_ode(y,t,f,h):
    k1=f(t,y)
    k2=f(t+h/2.0,y+(h/2.0)*k1)
    k3=f(t+h/2.0,y+(h/2.0)*k2)
    k4=f(t+h,y+h*k3)
    return y+(h/6.0)*(k1+2.0*k2+2.0*k3+k4)
```

### Ejemplo en 1-D

Resolviendo $\dot{y}(t) = y(t)(1-y(t))$ para distintas condiciones iniciales $y_0$, tenemos:

```python
# Logistic Equation
def my_f_1D(t,y):
    return np.array(y*(1-y))
```

```python
def plot_log_eq(h=0.15,solver='euler'):
    t_times = np.arange(0, 4, h)
    fig = plt.figure(figsize=(L/2,L/2))
    fig.clf()
    ax = fig.gca()
    ax.axis("equal")
    ax.grid(True)
    ax.set_title("Numerical Approximations and Direction Field")
    ax.axis([0, 4, -2, 2])

    for y0 in np.linspace(-1,5,40):
        y_output = np.zeros(t_times.size)
        y_output[0] = y0
        for i in range(1,t_times.size):
            if solver=='euler':
                y_output[i]=euler_ode(y_output[i-1],t_times[i-1],my_f_1D,h)
            elif solver=='RK2':
                y_output[i]=RK2_ode(y_output[i-1],t_times[i-1],my_f_1D,h)
            else:
                y_output[i]=RK4_ode(y_output[i-1],t_times[i-1],my_f_1D,h)
        plt.plot(t_times,y_output,'k-',alpha=0.5)
        plt.plot(t_times,y_output,'.')

    X,Y = np.meshgrid(np.arange(0,4,.2), np.arange(-2,2,.2))
    theta = np.arctan(my_f_1D(0,Y))
    U = np.cos(theta)
    V = np.sin(theta)
    plt.quiver(X,Y,U,V,alpha=0.5)
interact(plot_log_eq,h=(0.01,1,0.01),solver=['euler','RK2','RK4'])
```

    interactive(children=(FloatSlider(value=0.15, description='h', max=1.0, min=0.01, step=0.01), Dropdown(descrip…

    <function __main__.plot_log_eq(h=0.15, solver='euler')>

## Regiones de Estabilidad

Para analizar la estabilidad lineal de un método consideremos la ecuación diferencial $\dot{y} = \lambda y$, con $y(0)=1$, cuya solución es $y(t) = e^{\lambda t}$.

Para Euler tendríamos:

\begin{align*}
    y_{i+1} &= y_i + \lambda h y_i \\
    y_{i+1} &= (1 + \lambda h ) y_i \\
    y_{i+1} &= (1 + \lambda h )^{i+1} y_0
\end{align*}

Considerando la parte real de $\lambda$ negativa y $h$ positivo, para recrear la solución se necesita que $\left|1+\lambda h \right| <1$. Este dominio, se denomina la región de estabilidad del método.

Del mismo modo, para los demás métodos tenemos:

* RK2:
$$ \left|1+\lambda h + \dfrac{(\lambda h)^2}{2!} \right| <1 $$

* RK4:
$$ \left| 1+\lambda h + \dfrac{(\lambda h)^2}{2!} + \dfrac{(\lambda h)^3}{3!} + \dfrac{(\lambda h)^4}{4!} \right| <1 $$

```python
def zplot2(z, ax=plt.gca(), lw=1.5, line_color='k'):
    ax.plot(np.real(z), np.imag(z), line_color, lw=lw)
def runge_kutta_stability_regions():
    z = np.exp(1j * np.pi * np.arange(201)/100.)
    r = z-1
    d = 1-1./z;
    # Order 1
    W1, W2, W3, W4 = [0], [0], [0], [0]
    for zi in z[1:]:
        W1.append( W1[-1]-(1.+W1[-1]-zi) )
    for zi in z[1:]:
        W2.append( W2[-1]-(1+W2[-1]+.5*W2[-1]**2-zi**2)/(1+W2[-1]) )
    for zi in z[1:]:
        num = (1+W4[-1]+.5*W4[-1]**2+W4[-1]**3/6+W4[-1]**4/24-zi**4)
        den = (1+W4[-1]+W4[-1]**2/2+W4[-1]**3/6.)
        W4.append( W4[-1] - num/den )
    return W1, W2, W4
W1,W2,W4=runge_kutta_stability_regions()
```

```python
fig = plt.figure(figsize=(L/2,L/2))
ax=fig.gca()
zplot2(W1,ax,line_color='r')
zplot2(W2,ax,line_color='g')
zplot2(W4,ax,line_color='b')
ax.axis("equal")
ax.axis([-5, 2, -3.5, 3.5])
ax.grid("on")
ax.set_title("Stability Regions of some Runge-Kutta ODE solvers")
```

### Entonces, ¿cómo utilizamos la región de estabilidad?

Lo importante de este análisis es notar que no podemos definir $h$ a priori. Dados un método y $\lambda$ conocidos, debemos estudiar la región de estabilidad para determinar que valor de $h$ utilizar.

```python
def plot_log_eq_stability_region(y0=1.2, h=0.5, T=10):
    # How did I get this? Why do I need it?
    def my_f_1D_prime(y):
        return np.array(1-2*y)

    # Choosing which solver to use
    solvers = ('euler','RK2','RK4')
    solver = solvers[2]

    t_times = np.arange(0, T+h, h)
    y_output = np.zeros(t_times.size)
    y_output[0] = y0

    for i in range(1,t_times.size):
        if solver=='euler':
            y_output[i]=euler_ode(y_output[i-1],t_times[i-1],my_f_1D,h)
            c1,c2,c3='b','k','k'
        elif solver=='RK2':
            y_output[i]=RK2_ode(y_output[i-1],t_times[i-1],my_f_1D,h)
            c1,c2,c3='k','b','k'
        else:
            y_output[i]=RK4_ode(y_output[i-1],t_times[i-1],my_f_1D,h)
            c1,c2,c3='k','k','b'

    fig = plt.figure(figsize=(L,L))
    ax1 = fig.add_subplot(221)
    ax2 = fig.add_subplot(222)
    ax3 = fig.add_subplot(223)

    ax1.axis("equal")
    ax1.grid(True)
    ax1.set_title("Numerical Approximation")
    ax1.axis([0, T, -0.5, 2])
    ax1.plot(t_times,y_output,'-')
    ax1.plot(t_times,y_output,'.k')

    # The next code is to plot in logscale unstable numerical simulations
    y_all=h*my_f_1D_prime(y_output)
    y_pos=y_all>0
    y_neg=np.logical_not(y_pos)
    ax2.semilogy(t_times[y_pos],y_all[y_pos],'.r',ms=20)
    ax2.semilogy(t_times[y_neg],-y_all[y_neg],'.b',ms=20)
    ax2.grid("on")
    ax2.set_title("Numerical Approximation in log scale")

    zplot2(W1,ax3,line_color=c1)
    zplot2(W2,ax3,line_color=c2)
    zplot2(W4,ax3,line_color=c3)
    k_lambdah=h*my_f_1D_prime(y_output)
    ax3.plot(np.real(k_lambdah),np.imag(k_lambdah),'.r',ms=20)
    ax3.axis("equal")
    ax3.axis([-5, 2, -3.5, 3.5])
    ax3.grid("on")
    ax3.set_title("Stability Region and $k=\lambda\,h$")

#y0=1.2 # 1.2, 2.0, 4.0
#h=0.5  # 0.5, 0.1
#T=10
interact(plot_log_eq_stability_region,y0=(-2,5,0.1),h=(0.01,1,0.1), T=(1,20,1))
```

    interactive(children=(FloatSlider(value=1.2, description='y0', max=5.0, min=-2.0), FloatSlider(value=0.5, desc…

    <function __main__.plot_log_eq_stability_region(y0=1.2, h=0.5, T=10)>

## Convergencia

¡Esto es muy importante! Estudiémoslo...

Para hablar de convergencia, tenemos que tener en cuenta el orden de cada método. En palabras simples y resumidas podemos decir lo siguiente:

* Euler es un método de primer orden $O(h)$, por lo que si $h$ se reduce a la mitad, el error también lo hace.
* RK2 es un método de segundo orden $O(h^2)$, por lo que si $h$ se reduce a la mitad, el error se reduce a la cuarta parte.
* RK4 es un método de cuarto orden $O(h^4)$, por lo que si $h$ se reduce a la mitad, el error se reduce 16 veces.

Para ver esto, consideremos el siguiente problema:

\begin{align*}
\dot{y}(t)&=-3\,y(t)+6\,t+5\\
y(0)&=3
\end{align*}

cuya solución es $y(t)=2e^{-3t}+2t+1$.

```python
y0=3.0
time_test=0.5
def my_f(t,y):
    return np.array(-3*y+6*t+5)
y_sol = lambda t: 2.*np.exp(-3.*t)+2.*t+1
```

```python
fig = plt.figure(figsize=(L/2,L/2))
fig.clf()
ax = fig.gca()
ax.axis("equal")
ax.grid(True)
ax.set_title("Convergence Analysis")

h_list = np.logspace(-5,-1,5)
for h in h_list:
    t_times = np.arange(0, time_test+h, h)
    y_output = np.zeros(t_times.size)
    y_output[0] = y0
    for i in range(1,t_times.size):
        y_output[i]=euler_ode(y_output[i-1],t_times[i-1],my_f,h)
    plt.loglog(h,abs(y_output[-1]-y_sol(t_times[-1])),'b.',ms=20,label='Euler',alpha=.5)
    y_output = np.zeros(t_times.size)
    y_output[0] = y0
    for i in range(1,t_times.size):
        y_output[i]=RK2_ode(y_output[i-1],t_times[i-1],my_f,h)
    plt.loglog(h,abs(y_output[-1]-y_sol(t_times[-1])),'rs',ms=20,label='RK2',alpha=.5)
    y_output = np.zeros(t_times.size)
    y_output[0] = y0
    for i in range(1,t_times.size):
        y_output[i]=RK4_ode(y_output[i-1],t_times[i-1],my_f,h)
    plt.loglog(h,abs(y_output[-1]-y_sol(t_times[-1])),'gd',ms=20,label='RK4',alpha=.5)
    if h==h_list[0]:
        plt.legend(numpoints=1, loc="lower right")
plt.loglog(h_list,h_list,'k-')
plt.loglog(h_list,np.power(h_list,2.),'k-')
plt.loglog(h_list,np.power(h_list,4.),'k-')
```

# Problemas de segundo orden y más dimensiones

¿Por qué debemos estudiar problemas de dimensiones más altas?

¡Porque son geniales!

### Van der pool

El ocsilador Van der Pol es un oscilador no conservativo con amortiguamiento no lineal. Su evolución en el tiempo sigue una ecuación diferencial de segundo orden:

$$\ddot{y}-\mu\,(1-y^2)\,\dot{y} + y = 0$$ con $y(0)=2\ $   y   $\dot{y}(0)=0$

$\mu$ es un parámetro escalar que indica la no linealidad y la fuerza del amortiguamiento.

$y_1(t)=y(t)$

$y_2(t)=\dot{y}(t)$

$\dot{y}_1=\dot{y} = y_2$

$\dot{y}_2=\ddot{y} = \mu (1-y^2) \dot{y} - y = \mu (1-y_1^2) y_2 - y_1$

```python
mu=10
def my_vdp(t,y,mu=mu):
    y1 = y[0]
    y2 = y[1]
    return np.array([y2, mu*(1-y1**2)*y2-y1])

def my_vdp_eig_jacobian(t,y,mu=mu):
    J=[[0,1],[-(2*mu*y[0]*y[1]+1), mu*(1-y[0]**2)]]
    lambs,vs= LA.eig(J)
    return lambs
```

```python
def plotting_vdp(y01=2,y02=0,h=0.01,T=200,solver='euler'):
    y0=np.array([y01, y02])
    t_times = np.arange(0, T, h)
    y_output = np.zeros([t_times.size,2])
    y_output[0,:] = y0
    for i in range(1,t_times.size):
        #y_output[i,:]=euler_ode(y_output[i-1,:],t_times[i-1],my_vdp,h)
        if solver=='euler':
            y_output[i]=euler_ode(y_output[i-1],t_times[i-1],my_vdp,h)
        elif solver=='RK2':
            y_output[i]=RK2_ode(y_output[i-1],t_times[i-1],my_vdp,h)
        else:
            y_output[i]=RK4_ode(y_output[i-1],t_times[i-1],my_vdp,h)

    fig = plt.figure(figsize=(L,L/2))
    ax1 = fig.add_subplot(121)
    ax2 = fig.add_subplot(122)

    ax1.grid(True)
    ax1.set_title("Numerical Approximation")
    ax1.plot(t_times,y_output[:,0],'-')
    ax1.set(xlabel='t', ylabel='$y_1$')
    ax1.plot(t_times,y_output[:,1],'-r')

    ax2.grid(True)
    ax2.set_title("Phase Portrait")
    ax2.plot(y_output[:,0],y_output[:,1],'-')
    ax2.set(xlabel='$y=y_1$', ylabel='$\dot{y}=y_2$')

interact(plotting_vdp,y01=(-3,3,0.1),y02=(-3,3,0.1),h=(0.001,1,0.01),T=(10,200,10),solver=['euler','RK2','RK4'])
```

    interactive(children=(FloatSlider(value=2.0, description='y01', max=3.0, min=-3.0), FloatSlider(value=0.0, des…

    <function __main__.plotting_vdp(y01=2, y02=0, h=0.01, T=200, solver='euler')>

### ¿Qué hay acerca de la región de estabilidad en sistemas dinámicos de EDO's?

```python
def plotting_vpd_as_system(y01=2,y02=0,h=0.02,T=40,solver='euler'):
    t_times = np.arange(0, T+h, h)
    y_output = np.zeros([2,t_times.size])
    y_output[:,0] = y0

    for i in range(1,t_times.size):
        if solver=='euler':
            y_output[:,i]=euler_ode(y_output[:,i-1],t_times[i-1],my_vdp,h)
            c1='b'
            c2='k'
            c3='k'
        elif solver=='RK2':
            y_output[:,i]=RK2_ode(y_output[:,i-1],t_times[i-1],my_vdp,h)
            c1='k'
            c2='b'
            c3='k'
        else:
            y_output[:,i]=RK4_ode(y_output[:,i-1],t_times[i-1],my_vdp,h)
            c1='k'
            c2='k'
            c3='b'

    fig = plt.figure(figsize=(L,L))
    ax1 = fig.add_subplot(221)
    ax2 = fig.add_subplot(222)
    ax3 = fig.add_subplot(223)

    ax1.axis('equal')
    ax1.grid(True)
    ax1.set_title("Numerical Approximation")
    ax1.axis([0, T, -0.5, 2])
    ax1.plot(t_times,y_output[0,:],'-')
    ax1.plot(t_times,y_output[1,:],'r-')

    zplot2(W1,ax2,line_color=c1)
    zplot2(W2,ax2,line_color=c2)
    zplot2(W4,ax2,line_color=c3)
    for i in range(1,t_times.size):
        k_lambdah=h*my_vdp_eig_jacobian(t_times[i],y_output[:,i])
        ax2.plot(np.real(k_lambdah[0]),np.imag(k_lambdah[0]),'.r',ms=10,alpha=.4)
        ax2.plot(np.real(k_lambdah[1]),np.imag(k_lambdah[1]),'sm',ms=10,alpha=.4)
    ax2.axis('equal')
    ax2.axis([-5, 2, -3.5, 3.5])
    ax2.grid('on')

    ax3.grid(True)
    ax3.set_title("Phase Portrait")
    ax3.plot(y_output[0,:],y_output[1,:],'-')
    ax3.set(xlabel='$y=y_1$', ylabel='$\dot{y}=y_2$')

interact(plotting_vpd_as_system,y01=(-3,3,0.1),y02=(-3,-3,0.1),h=(0.01,1,0.01),T=(10,200,10),solver=['euler','RK2','RK4'])
```

    interactive(children=(FloatSlider(value=2.0, description='y01', max=3.0, min=-3.0), FloatSlider(value=-3.0, de…

    <function __main__.plotting_vpd_as_system(y01=2, y02=0, h=0.02, T=40, solver='euler')>

### Modelo Lotka-Volterra (ecuaciones predador-presa), es decir, lobos comiendo conejos y conejos escondiendo y comiendo pasto infinito.

Se definen $y_1(t)$ como el número de conejos en el tiempo $t$ y $y_2(t)$ como el número de lobos en el tiempo $t$.
Todo esto está pasando en una isla, aunque estamos considerando la presencia de pasto infinito. Entonces, si no hay lobos, obtenemos $\dot{y1}(t)=y_1(t)$, en otras palabras, el número de conejos crece exponencialmente.
Ahora, si no hubiesen conejos, obtenemos $\dot{y_2}(t)=-y_2(t)$, debido a que la población de lobos decae exponencialmente. Pero, ¿qué pasa si tenemos ambas especies?

\begin{align*}
\dot{y_1}(t)&=(1-y_2(t)/\mu_2)\,y_1(t)\\
\dot{y_2}(t)&=-(1-y_1(t)/\mu_1)\,y_2(t)\\
y_1(0)&=400\\
y_2(0)&=100
\end{align*}

donde $\mu_1$ y $\mu_2$ son constantes de normalización.

```python
mu1=300
mu2=200
def f_predprey(t,y,mu1=mu1,mu2=mu2):
    y1 = y[0]
    y2 = y[1]
    return np.array([(1-y2/mu2)*y1, -(1-y1/mu1)*y2])

def f_predprey_jacobian(t,y,mu1=mu1,mu2=mu2):
    J=[[1-y[1]/mu2,-y[0]/mu2],[y[1]/mu1, -(1-y[0]/mu1)]]
    lambs,vs= LA.eig(J)
    return lambs
```

```python
y0=[10, 100]
h=0.03
T=30

# Choosing which solver to use
solvers = ('euler','RK2','RK4')
solver = solvers[0]

def plotting_predprey(y01=70,y02=100,h=0.03,T=30,solver='euler'):
    t_times = np.arange(0, T+h, h)
    y_output = np.zeros([2,t_times.size])
    y_output[:,0] = y0
    y_output[0,0] = y01
    y_output[1,0] = y02

    for i in range(1,t_times.size):
        if solver=='euler':
            y_output[:,i]=euler_ode(y_output[:,i-1],t_times[i-1],f_predprey,h)
            c1='b'
            c2='k'
            c3='k'
        elif solver=='RK2':
            y_output[:,i]=RK2_ode(y_output[:,i-1],t_times[i-1],f_predprey,h)
            c1='k'
            c2='b'
            c3='k'
        else:
            y_output[:,i]=RK4_ode(y_output[:,i-1],t_times[i-1],f_predprey,h)
            c1='k'
            c2='k'
            c3='b'

    fig = plt.figure(figsize=(L,L))
    ax1 = fig.add_subplot(221)
    ax2 = fig.add_subplot(222)
    ax3 = fig.add_subplot(223)

    ax1.grid(True)
    ax1.set_title('Numerical Approximation')
    ax1.plot(t_times,y_output[0,:],'-')
    ax1.plot(t_times,y_output[1,:],'r-')
    ax1.axis([0, T, 0, 600])

    zplot2(W1,ax2,line_color=c1)
    zplot2(W2,ax2,line_color=c2)
    zplot2(W4,ax2,line_color=c3)
    for i in range(1,t_times.size):
        k_lambdah=h*my_vdp_eig_jacobian(t_times[i],y_output[:,i])
        ax2.plot(np.real(k_lambdah[0]),np.imag(k_lambdah[0]),'.r',ms=10,alpha=.4)
        ax2.plot(np.real(k_lambdah[1]),np.imag(k_lambdah[1]),'sm',ms=10,alpha=.4)
    ax2.axis('equal')
    ax2.axis([-5, 2, -3.5, 3.5])
    ax2.grid('on')

    ax3.grid(True)
    ax3.set_title('Phase Portrait')
    ax3.plot(y_output[0,:],y_output[1,:],'-')
    ax3.set(xlabel='$y_1$', ylabel='$y_2$')
    ax3.axis([0, 800, 0, 600])

interact(plotting_predprey,y01=(0,1000,10),y02=(0,1000,10),h=(0.01,1,0.01),T=(10,200,10),solver=['euler','RK2','RK4'])
```

    interactive(children=(IntSlider(value=70, description='y01', max=1000, step=10), IntSlider(value=100, descript…

    <function __main__.plotting_predprey(y01=70, y02=100, h=0.03, T=30, solver='euler')>

```python
def f_trig(t,y):
    y1 = y[0]
    y2 = y[1]
    return np.array([y2, -y1])

def f_trig_jacobian(t,y):
    #J=[[0,1],[-1, 0]]
    #lambs,vs= LA.eig(J)
    lambs=np.array([0.+1.j, 0.-1.j])
    return lambs

def plotting_f_trig(y01=1,y02=0,h=0.2,T=100,solver='euler'):

    t_times = np.arange(0, T+h, h)
    y_output = np.zeros([2,t_times.size])
    y_output[:,0] = y0

    for i in range(1,t_times.size):
        if solver=='euler':
            y_output[:,i]=euler_ode(y_output[:,i-1],t_times[i-1],f_trig,h)
            c1='b'
            c2='k'
            c3='k'
        elif solver=='RK2':
            y_output[:,i]=RK2_ode(y_output[:,i-1],t_times[i-1],f_trig,h)
            c1='k'
            c2='b'
            c3='k'
        else:
            y_output[:,i]=RK4_ode(y_output[:,i-1],t_times[i-1],f_trig,h)
            c1='k'
            c2='k'
            c3='b'
    fig = plt.figure(figsize=(L,L))
    ax1 = fig.add_subplot(221)
    ax2 = fig.add_subplot(222)
    ax3 = fig.add_subplot(223)

    ax1.grid(True)
    ax1.set_title('Numerical Approximation')
    ax1.plot(t_times,y_output[0,:],'-')
    ax1.plot(t_times,y_output[1,:],'r-')

    zplot2(W1,ax2,line_color=c1)
    zplot2(W2,ax2,line_color=c2)
    zplot2(W4,ax2,line_color=c3)
    for i in range(1,t_times.size):
        k_lambdah=h*f_trig_jacobian(t_times[i],y_output[:,i])
        ax2.plot(np.real(k_lambdah[0]),np.imag(k_lambdah[0]),'.r',ms=10,alpha=.4)
        ax2.plot(np.real(k_lambdah[1]),np.imag(k_lambdah[1]),'sm',ms=10,alpha=.4)
    ax2.axis('equal')
    ax2.axis([-5, 2, -3.5, 3.5])
    ax2.grid('on')

    ax3.grid(True)
    ax3.set_title('Phase Portrait')
    ax3.plot(y_output[0,:],y_output[1,:],'-')
    ax3.set(xlabel='$y_1$', ylabel='$y_2$')

interact(plotting_f_trig,y01=(-2,2,0.1),y02=(-2,2,0.1),h=(0.01,1,0.01),T=(10,200,10),solver=['euler','RK2','RK4'])
```

    interactive(children=(FloatSlider(value=1.0, description='y01', max=2.0, min=-2.0), FloatSlider(value=0.0, des…

    <function __main__.plotting_f_trig(y01=1, y02=0, h=0.2, T=100, solver='euler')>

```python
def f_exp(t,y):
    y1 = y[0]
    return np.array([-y1])
def f_exp_jac(t,y):
    return -1

def plotting_f_exp(y0=1,h=0.1,T=10,solver='euler'):

    t_times = np.arange(0, T+h, h)
    y_output = np.zeros([2,t_times.size])
    y_output[:,0] = y0

    for i in range(1,t_times.size):
        if solver=='euler':
            y_output[:,i]=euler_ode(y_output[:,i-1],t_times[i-1],f_exp,h)
            c1='b'
            c2='k'
            c3='k'
        elif solver=='RK2':
            y_output[:,i]=RK2_ode(y_output[:,i-1],t_times[i-1],f_exp,h)
            c1='k'
            c2='b'
            c3='k'
        else:
            y_output[:,i]=RK4_ode(y_output[:,i-1],t_times[i-1],f_exp,h)
            c1='k'
            c2='k'
            c3='b'

    fig = plt.figure(figsize=(L,L/2))
    ax1 = fig.add_subplot(121)
    ax2 = fig.add_subplot(122)

    ax1.grid(True)
    ax1.set_title('Numerical Approximation')
    ax1.plot(t_times,y_output[0,:],'.k')
    ax1.plot(t_times,y_output[0,:],'-',alpha=0.5)
    ax1.axis([0, T, -2, 2])

    zplot2(W1,ax2,line_color=c1)
    zplot2(W2,ax2,line_color=c2)
    zplot2(W4,ax2,line_color=c3)
    for i in range(1,t_times.size):
        k_lambdah=h*f_exp_jac(t_times[i],y_output[:,i])
        ax2.plot(np.real(k_lambdah),np.imag(k_lambdah),'.r',ms=10,alpha=.4)
    ax2.axis('equal')
    ax2.axis([-5, 2, -3.5, 3.5])
    ax2.grid('on')

interact(plotting_f_exp,y0=(-2,2,0.1),h=(0.01,10,0.01),T=(1,20,1),solver=['euler','RK2','RK4'])
```

    interactive(children=(FloatSlider(value=1.0, description='y0', max=2.0, min=-2.0), FloatSlider(value=0.1, desc…

    <function __main__.plotting_f_exp(y0=1, h=0.1, T=10, solver='euler')>

# Problemas de Valor de Frontera

Un problema de valor de frontera (BVP) corresponde a una ecuación diferencial ordinaria de la forma:
$$ y''(x) = f(x,y(x),y'(x)) $$
sujeta a condiciones de borde:
$$ y(a) = y_a $$
$$ y(b) = y_b $$
Intentemos resolver $y''(x)=4y(x)$, con $y(0)=y_0$ y $y(1)=y_n$.

## Diferencias Finitas

Este método consiste en aproximar las derivadas de $y(x)$ mediante diferencias finitas, para luego resolver un sistema de ecuaciones lineales o no lineal.

Dentro de las más conocidas están:

* Forward Difference:
$$ y'(x) = \dfrac{y(x+h) - y(x)}{h} + O(h)$$

* Backward Difference:
$$ y'(x) = \dfrac{y(x) - y(x-h)}{h} + O(h)$$

* Central Difference:
$$ y'(x) = \dfrac{y(x+h) - y(x-h)}{2h} + O(h^2)$$

Y para aproximar la segunda derivada utilizamos:
\begin{align*}
y''(x) &= \dfrac{\text{Forward Difference } - \text{ Backward Difference}}{h} + O(h^2) \\
y''(x) &= \dfrac{\dfrac{y(x+h) - y(x)}{h} - \dfrac{y(x) - y(x-h)}{h}}{h} + O(h^2) \\
y''(x) &= \dfrac{y(x+h) - 2y(x) + y(x-h)}{h^2} + O(h^2) \\
\end{align*}

```python
def solve_finite_difference_eq(I=4,y0=1,yn=3):
    # Spatial discretization, 'I' represents the number of intervals to be used.
    h=(1.-0)/I
    ## Boundarty conditions
    #y0=1.
    #y1=3.

    # Building Finite Difference Discretization
    deltas=-(2.+4.*(h**2.))
    A=toeplitz(np.append(np.array([deltas, 1.]), np.zeros(I-3)))
    # Building RHS
    b=np.append(-y0, np.zeros(I-3))
    b=np.append(b,-yn)
    # Solving the linear system of equations
    w=np.linalg.solve(A, b)
    # Adding back the boundary conditions into the solution
    w=np.append([y0], w)
    w=np.append(w,[yn])
    t_FD = np.linspace(0,1,I+1)
    w_FD = w
    return t_FD, w_FD, A

def plot_solution_finite_difference_eq(I=4,y0=1,yn=3):

    # Solving by Finite Difference
    t_FD, w_FD, A = solve_finite_difference_eq(I,y0,yn)

    # Plotting
    fig = plt.figure(figsize=(L,L/2))
    ax1 = fig.add_subplot(121)
    ax2 = fig.add_subplot(122)

    # Plotting the discrete solution
    ax1.grid(True)
    ax1.set_title("FD")
    ax1.plot(t_FD,w_FD,'.',ms=20)
    #ax1.axis([0, 1, 0, 3])
    ax1.grid("on")

    # Plotting the pattern and coefficients of the tri-diagonal matrix
    plot_matrices_with_values(ax2,A)

interact(plot_solution_finite_difference_eq,I=(3,25,1),y0=(-5,5,0.1),yn=(-5,5,0.1))
```

    interactive(children=(IntSlider(value=4, description='I', max=25, min=3), FloatSlider(value=1.0, description='…

    <function __main__.plot_solution_finite_difference_eq(I=4, y0=1, yn=3)>

## Método de Disparo

Este método consiste en tratar el BVP como si fuera un IVP mediante las siguientes consideraciones:

* Teniendo el BVP $ y''(x) = f(x,y(x),y'(x)) $,con $ y(a) = y_a $ y $ y(b) = y_b $.
* Consideremos $x \rightarrow t$ y $ \ddot{y}(t) = f(t,y(t),\dot{y}(t)) $,con $ y(t_0) = y_0 $ y $ \dot{y}(t_0) = \alpha $.
* Y definamos la función $F(\alpha) = y(t_n,\alpha) - y_b$.

Entonces solo basta con encontrar una raíz de $F(\alpha)$ para que la solución de este IVP corresponda a la solución del BVP que en realidad estamos resolviendo.

```python
def my_f(t,y):
    return np.array([y[1],4*y[0]])
```

```python
alpha=-1.
N=100

def shooting_method_101(alpha=-1, N=50, I=4, y0=1, yn=3):
    T=1.0
    h=T/(N-1.)
    t_times = np.linspace(0,T,num=N)
    y_output = np.zeros([t_times.size,2])
    y_output[0,:] = [y0,alpha]
    for i in range(1,t_times.size):
        y_output[i,:]=euler_ode(y_output[i-1,:],t_times[i-1],my_f,h)

    fig = plt.figure(figsize=(L/2,L/2))
    ax = fig.gca()
    plt.grid(True)
    plt.title("Numerical Approximation")
    plt.plot(t_times,y_output[:,0],'rd',ms=12,alpha=0.5,label='y: actual solution')
    plt.plot(t_times,y_output[:,1],'m.',ms=12,alpha=0.5,label='$\dot{y}$: derivative of solution')

    # Solving by Finite Difference
    t_FD, w_FD, _ = solve_finite_difference_eq(I,y0,yn)
    plt.plot(t_FD,w_FD,'.',ms=20,alpha=1)

    # Plotting distance to be reduced, so the missing initial condition is found.
    plt.plot([t_FD[-1],t_times[-1]],[w_FD[-1],y_output[-1,0]],'g-',label='Difference to be reduced')

    plt.plot(0,1,'.k',ms=12,label='Left Boundary Condition')
    plt.plot(1,3,'.k',ms=12,label='Right Boundary Condition')

    plt.legend()

interact(shooting_method_101,alpha=(-2,2,0.1),N=(10,200,10),I=(3,20), y0=(-5,5,0.1),yn=(-5,5,0.1))
```

    interactive(children=(FloatSlider(value=-1.0, description='alpha', max=2.0, min=-2.0), IntSlider(value=50, des…

    <function __main__.shooting_method_101(alpha=-1, N=50, I=4, y0=1, yn=3)>

### Ahora hagámoslo automáticamente

```python
N_grid = 100
def func(alpha):
    yn=3
    t_times,y_output = my_solver(N_grid,alpha)
    return y_output[-1,0]-yn

def my_solver(N,alpha):
    y0=1.
    T=1.0
    h=T/(N-1.)
    t_times = np.linspace(0,T,num=N)
    y_output = np.zeros([t_times.size,2])
    y_output[0,:] = [y0,alpha]
    for i in range(1,t_times.size):
        y_output[i,:]=RK4_ode(y_output[i-1,:],t_times[i-1],my_f,h)
    return t_times,y_output
```

```python
alpha = root(func, 0.)

t_times,y_output = my_solver(N_grid,alpha.x)

fig = plt.figure(figsize=(L/2,L/2))
#ax = fig.gca()
plt.grid(True)
plt.title("Numerical Approximation")
plt.plot(t_times,y_output[:,0],'rd',ms=20,alpha=0.5)
t_FD, w_FD, _ = solve_finite_difference_eq()
plt.plot(t_FD,w_FD,'.',ms=20,alpha=1)
```

```python
alpha
```

        fjac: array([[-1.]])
         fun: 0.0
     message: 'The solution converged.'
        nfev: 5
         qtf: array([-1.33226763e-15])
           r: array([-1.81343016])
      status: 1
     success: True
           x: array([-0.42030605])

```python
N = 100
alphas = np.linspace(-1,0,N)
fa = []
for a in alphas:
    fa.append(func(a))
faa = np.array(fa)
plt.figure()
plt.plot(alphas,faa)
plt.grid(True)
plt.plot([alpha.x],[0],'.k')
plt.show()
```

# Acknowledgements
* _Material creado por profesor Claudio Torres_ (`ctorres@inf.utfsm.cl`) _y ayudantes: Alvaro Salinas y Martín
Villanueva. DI UTFSM. Abril 2016._

***
### DISCLAIMER ###

El presente notebook ha sido creado para el curso **ILI286 - Computación Científica 2**, del [Departamento de Informática](http://www.inf.utfsm.cl/), [Universidad Técnica Federico Santa María](http://www.utfsm.cl/).

El material ha sido creado por Claudio Torres <ctorres@inf.utfsm.cl> y Sebastian Flores <sebastian.flores@usm.cl>, y es distribuido sin restricciones. En caso de encontrar un error, por favor no dude en contactarnos.

[Update 2015] Se ha actualizado los notebooks a Python 3 e includio el "magic" "%matplotlib inline" antes de cargar matplotlib para que los gráficos se generen en el notebook.

[Update 2016] (Álvaro) Modificaciones mayores al formato original. Agregado contexto: Introducción, Tabla de Contenidos, Explicaciones de cada método.

[Update 2019] (C. Torres) Small changes. Fixing issue with title of sections and identation. Adding 'interact' to the logistic equation! Adding interact to everything, work in progress. All done, Enjoy!
***

```python

```


```python
%matplotlib widget
```

```python
import numpy as np
import matplotlib.pyplot as plt
import pydae.ssa as ssa
import scipy.signal as sctrl
```

```python
from vsc_lcl import vsc_lcl_class
```

## Instantiate system

```python
syst = vsc_lcl_class()
syst.Dt = 5e-6
syst.decimation = 1
syst.N_store = 100_000
syst.update()
```

## CTRL1 in state feedback

```python
syst = vsc_lcl_class()
syst.Dt = 5e-6
syst.decimation =1
syst.N_store =100_000
syst.update()

Δt = 50e-6
#x_d_ctrl_list = ['i']   # states to consider in the reduction
z_ctrl_list = [ 'i_sd_g01', 'i_sq_g01']     # outputs to consider in the controller
u_ctrl_list = ['eta_d_g01','eta_q_g01']     # intputs to consider in the controller
z_ctrl_idxs = [syst.outputs_list.index(item) for item in z_ctrl_list]
u_ctrl_idxs = [syst.inputs_run_list.index(item) for item in u_ctrl_list]
u_pert_list = ['v_sD_g01','v_sQ_g01']     # intputs to consider in the controller
u_pert_idxs = [syst.outputs_list.index(item) for item in z_pert_list]

syst.Δt = Δt

## Calculate equilibirum point
syst.initialize([{'G_d_g01':0.0,'eta_d_g01':0.0,'eta_q_g01':-0.8693333,'v_1_Q':-326,'v_1_D':0.0, 'C_m_g01':4e-6}],xy0=1000)
ssa.eval_ss(syst)

# linear continous plant
A_p = syst.A
B_p = syst.B
C_p = syst.C
D_p = syst.D

# plant discretization
A_d,B_d,C_d,D_d,Dt = sctrl.cont2discrete((A_p,B_p,C_p,D_p),Δt,method='zoh')

N_z_d,N_x_d = C_d.shape  # discreticed plant dimensions
N_x_d,N_u_d = B_d.shape

# convenient matrices
O_ux = np.zeros((N_u_d,N_x_d))
O_xu = np.zeros((N_x_d,N_u_d))
O_uu = np.zeros((N_u_d,N_u_d))
I_uu = np.eye(N_u_d)

syst.A_d = A_d
syst.B_d = B_d

# Controller ##################################################################################
B_c = B_d[:,u_ctrl_idxs]
C_c = C_d[z_ctrl_idxs,:]
D_c = D_d[z_ctrl_idxs,:][:,u_ctrl_idxs]

N_x_c,N_u_d = B_c.shape
N_z_c,N_x_c = C_c.shape

O_ux = np.zeros((N_u_d,N_x_d))
O_xu = np.zeros((N_x_d,N_u_d))
O_uu = np.zeros((N_u_d,N_u_d))
I_uu = np.eye(N_u_d)

# discretized plant:
# Δx_d = A_d*Δx_d + B_d*Δu_d
# Δz_c = C_c*Δx_d + D_c*Δu_d

# dinamic extension:
# Δx_d = A_d*Δx_d + B_d*Δu_d
# Δx_i = Δx_i + Δt*(Δz_c-Δz_c_ref) = Δx_i + Δt*C_c*Δx_d - Dt*Δz_c_ref
# Δz_c = z_c - z_c_0
# Δz_c_ref = z_c_ref - z_c_0
# (Δz_c-Δz_c_ref) = z_c - z_c_ref
omega_b = 2*np.pi*50

W =  np.block([
                [   np.cos(omega_b*Δt), -np.sin(omega_b*Δt)],
                [   np.sin(omega_b*Δt),  np.cos(omega_b*Δt)],
               ])
W = np.eye(2)
A_e = np.block([
                [    A_d,  B_c@W, O_xu],    # Δx_d
                [   O_ux,   O_uu, O_uu],    # Δx_r
                [ Δt*C_c, Δt*D_c, I_uu],    # Δx_i
               ])

B_e = np.block([
                [   O_xu],
                [   I_uu],
                [   O_uu],
               ])

A_ctrl = A_e[N_x_d:,N_x_d:]
B_ctrl = B_e[N_x_d:]

# weighting matrices
Q_c = np.eye(A_e.shape[0])
Q_c[-1,-1] = 1e6
Q_c[-2,-2] = 1e6

R_c = np.eye(B_c.shape[1])*100000

K_c,S_c,E_c = ssa.dlqr(A_e,B_e,Q_c,R_c)

E_cont = np.log(E_c)/Δt

syst.A_ctrl = A_ctrl
syst.B_ctrl = B_ctrl
syst.K_c = K_c
syst.N_x_d = N_x_d  # number of plant states
syst.N_u_d = N_u_d  # number of plant inputs
syst.N_z_c = N_z_c  # number of plant outputs considered for the controller
```

## Observer

```python
z_obs_list =  ['v_md_g01', 'v_mq_g01','i_sd_g01', 'i_sq_g01']       # outputs to consider in the observer
z_obs_idxs  = [syst.outputs_list.index(item) for item in z_obs_list]

# discretized plant:
# Dx_d = A_d*Dx_d + B_d*Du_d
# z_o  = C_o*Dx_d + D_o*Du_d

# x_o = A_d*x_o + B_d*u_d + L_o*(z_o - C_o*x_o - D_o*Du_d)
B_o = B_d[:,u_ctrl_idxs]
C_o = C_d[z_obs_idxs,:]
D_o = D_d[z_obs_idxs,:][:,u_ctrl_idxs]
B_pert = B_d[:,[1,2]]
N_z_o = C_o.shape[0]

Q_o = np.eye(A_d.shape[0])

R_o = np.diag([1]*N_z_o)
K_o_T,S_o,E_o = ssa.dlqr(A_d.T,C_o.T,Q_o,R_o)
K_o = K_o_T.T

syst.K_o = K_o
syst.C_o = C_o
syst.D_o = D_o

syst.z_obs_list = z_obs_list
syst.N_z_o = N_z_o
syst.x_obs = np.zeros((N_x_d,1))

print('damp_ctrl',-E_c.real/np.abs(E_c))
print('damp_obs',-E_o.real/np.abs(E_o))
```

    damp_ctrl [-0.48951411 -0.48951411 -0.51666281 -0.51666281 -0.99964555 -0.99964555
     -0.9999404  -0.9999404  -1.          1.        ]
    damp_obs [-0.99987663 -0.99987663 -0.11391326 -0.11391326 -0.08265076 -0.08265076]

## Performance assessment

```python
syst.Dt = 5e-6
syst.decimation =1
syst.N_store =100_000
syst.update()
times = np.arange(0.0,0.1,Δt)

syst.initialize([{'G_d_g01':0.0,'eta_d_g01':0.0,'eta_q_g01':-0.8693333,'v_1_Q':-326,'v_1_D':0.0, 'C_m_g01':4e-6}],xy0=1000)
ssa.eval_A(syst)
i_sd = syst.get_value('i_sd_g01')
i_sq = syst.get_value('i_sq_g01')
v_sd = syst.get_value('v_sd_g01')
v_sq = syst.get_value('v_sq_g01')
i_td = syst.get_value('i_td_g01')
i_tq = syst.get_value('i_tq_g01')
v_md = syst.get_value('v_md_g01')
v_mq = syst.get_value('v_mq_g01')
v_dc = syst.get_value('v_dc_g01')
eta_d = syst.get_value('eta_d_g01')
eta_q = syst.get_value('eta_q_g01')
i_sd_ref_0 = i_sd
i_sq_ref_0 = i_sq
v_sq_0 = v_sq
v_sd_0 = v_sd
x_d_0 = np.array([i_td,i_tq,v_md,v_mq,i_sd,i_sq]).reshape(6,1)
u_d_0 = np.array([eta_d,eta_q]).reshape(2,1)
x_r_0 = u_d_0
syst.Δx_e = np.zeros((10,1))

# outputs for observer
z_obs_values_list = []
for item in syst.z_obs_list:
     z_obs_values_list += [syst.get_value(item)]
z_o_0 = np.array(z_obs_values_list).reshape(N_z_o,1)
syst.Δx_o = np.zeros((6,1))
ΔX_o = np.zeros((len(times),6))

it = 0
for t in times:

    Δx_e = syst.Δx_e
    Δx_o = syst.Δx_o

    # measurements
    i_sd = syst.get_value('i_sd_g01')
    i_sq = syst.get_value('i_sq_g01')
    v_sd = syst.get_value('v_sd_g01')
    v_sq = syst.get_value('v_sq_g01')
    i_td = syst.get_value('i_td_g01')
    i_tq = syst.get_value('i_tq_g01')
    v_md = syst.get_value('v_md_g01')
    v_mq = syst.get_value('v_mq_g01')
    v_dc = syst.get_value('v_dc_g01')

    x_d = np.array([i_td,i_tq,v_md,v_mq,i_sd,i_sq]).reshape(6,1)

    Δx_d = x_d - x_d_0
    Δx_d = Δx_o
    Δx_r = syst.Δx_e[N_x_c:-N_u_d,:]
    Δx_i = syst.Δx_e[(N_x_c+N_u_d):,:]

    # outputs for observer
    z_obs_values_list = []
    for item in syst.z_obs_list:
         z_obs_values_list += [syst.get_value(item)]
    z_o = np.array(z_obs_values_list).reshape(N_z_o,1)

    Δz_o = z_o - z_o_0

    # observer dynamics
    Δu_pert = np.block([[ (v_sd-v_sd_0)],[(v_sq-v_sq_0)]])

    Δx_o = A_d @ Δx_o + B_o@(Δx_r) + B_pert@Δu_pert  +  K_o @ (Δz_o - C_o @ Δx_o - D_o @ (Δx_r-Δx_pert))

    i_sd_ref = i_sd_ref_0
    i_sq_ref = i_sq_ref_0
    v_sq = v_sq_0
    v_sd = v_sd_0
    if t>20e-3: i_sd_ref = 20
    if t>30e-3: i_sq_ref = 30
    if t>45e-3: v_sd =  163
    if t>45e-3: v_sq = -163
    epsilon_d = i_sd - i_sd_ref
    epsilon_q = i_sq - i_sq_ref

    epsilon = np.block([[epsilon_d],[epsilon_q]])

    Δu_r = -K_c @ Δx_e + np.block([[ (v_sd-v_sd_0)*2/v_dc],[(v_sq-v_sq_0)*2/v_dc]])

    Δx_r = Δu_r
    Δx_i += Δt*epsilon

    Δx_e = np.block([[Δx_d],[Δx_r],[Δx_i]])

    x_r = Δx_r + x_r_0

    eta_dq = x_r
    eta_d = eta_dq[0,0]
    eta_q = eta_dq[1,0]

    events=[{'t_end':t,'eta_d_g01':eta_d,'eta_q_g01':eta_q,'v_1_Q':v_sq,'v_1_D':v_sd}]
    syst.run(events)

#    eta_d_prev = eta_d
#    eta_q_prev = eta_q

    syst.Δx_e =  Δx_e
    syst.Δx_o =  Δx_o

    ΔX_o[it,:] = Δx_o.T

    it += 1

syst.post();

```

```python
plt.close('all')
fig, axes = plt.subplots(nrows=2, ncols=1, figsize=(7, 7),sharex=True)

lines = axes[0].plot(syst.T,syst.get_values('i_sd_g01'),label='i_sd_g01')
lines = axes[0].plot(syst.T,syst.get_values('i_sq_g01'),label='i_sq_g01')

axes[1].plot(syst.T,syst.get_values('eta_D_g01'),label='eta_D_g01')
axes[1].plot(syst.T,syst.get_values('eta_Q_g01'),label='eta_Q_g01')
for ax in axes:
    ax.grid()
    ax.legend()
ax.set_xlabel('Time (s)')

```

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

    Text(0.5, 0, 'Time (s)')

```python
plt.close('all')
fig, axes = plt.subplots(nrows=3, ncols=1, figsize=(7, 7),sharex=True)

axes[0].plot(syst.T,syst.get_values('i_td_g01'),label='i_td')
axes[0].plot(syst.T,syst.get_values('i_tq_g01'),label='i_tq')
axes[0].plot(times,ΔX_o[:,0],label='i_sd est.')
axes[0].plot(times,ΔX_o[:,1],label='i_sq est.')

axes[1].plot(syst.T,syst.get_values('v_md_g01'),label='v_md')
axes[1].plot(syst.T,syst.get_values('v_mq_g01'),label='v_mq')
axes[1].plot(times,ΔX_o[:,2]+syst.get_values('v_md_g01')[0],label='v_md est.')
axes[1].plot(times,ΔX_o[:,3]+syst.get_values('v_mq_g01')[0],label='v_mq est.')

axes[2].plot(syst.T,syst.get_values('i_sd_g01'),label='i_sd')
axes[2].plot(syst.T,syst.get_values('i_sq_g01'),label='i_sq')
axes[2].plot(times,ΔX_o[:,4]+syst.get_values('i_sd_g01')[0],label='i_sd est.')
axes[2].plot(times,ΔX_o[:,5]+syst.get_values('i_sq_g01')[0],label='i_sq est.')

for ax in axes:
    ax.grid()
    ax.legend()
ax.set_xlabel('Time (s)')

```

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

    Text(0.5, 0, 'Time (s)')

## Simulink

```python
import sympy as sym

x_d_1,x_d_2,x_d_3,x_d_4,x_d_5,x_d_6 = sym.symbols('Dx_d_1,Dx_d_2,Dx_d_3,Dx_d_4,Dx_d_5,Dx_d_6')
x_r_1,x_r_2 = sym.symbols('Dx_r_1,Dx_r_2')
x_i_1,x_i_2 = sym.symbols('Dx_i_1,Dx_i_2')

x_e = sym.Matrix([x_d_1,x_d_2,x_d_3,x_d_4,x_d_5,x_d_6,x_r_1,x_r_2,x_i_1,x_i_2])
u_r = -K_c * x_e

u_r_d = str(sym.N(u_r[0],8))
u_r_q = str(sym.N(u_r[1],8))

print(f'Du_r_1 = {u_r_d};')
print(f'Du_r_2 = {u_r_q};')
print('\nWarning: Control output is eta_dq!!')
```

    Du_r_1 = -0.013361535*Dx_d_1 - 0.00027748637*Dx_d_2 + 0.0021866204*Dx_d_3 + 5.2865939e-5*Dx_d_4 + 0.0071983421*Dx_d_5 + 0.00022706741*Dx_d_6 - 2.2443754*Dx_i_1 + 0.68967688*Dx_i_2 - 0.36460717*Dx_r_1 - 0.0049366387*Dx_r_2;
    Du_r_2 = 0.00027748637*Dx_d_1 - 0.013361535*Dx_d_2 - 5.2865939e-5*Dx_d_3 + 0.0021866204*Dx_d_4 - 0.00022706741*Dx_d_5 + 0.0071983421*Dx_d_6 - 0.68967688*Dx_i_1 - 2.2443754*Dx_i_2 + 0.0049366387*Dx_r_1 - 0.36460717*Dx_r_2;

    Warning: Control output is eta_dq!!

```python
it_ini = 4
Δx_o = sym.Matrix([sym.Symbol(f'xD[{it+it_ini}]') for it in range(6)])
Δz_o = sym.Matrix([sym.Symbol(item) - sym.Symbol(item+'_0') for item in ['v_md', 'v_mq', 'i_sd', 'i_sq']])
Δx_r = sym.Matrix([sym.Symbol(f'Dx_r_{it+1}') for it in range(2)])
Δu_pert = sym.Matrix([sym.Symbol(f'Du_pert_{it+1}') for it in range(2)])

Δx_o_kp1 = A_d @ Δx_o + B_o@(Δx_r) +  K_o @ (Δz_o - C_o @ Δx_o) # + B_pert@Δu_pert  +  K_o @ (Δz_o - C_o @ Δx_o - D_o @ (Δx_r))

for it in range(6):
    print(f'xD[{it+it_ini}] = {Δx_o_kp1[it]};')
```

    xD[4] = 13.7996619184135*Dx_r_1 + 0.103767816974477*Dx_r_2 + 0.290189894236834*i_sd - 0.290189894236834*i_sd_0 + 0.00455866714091358*i_sq - 0.00455866714091358*i_sq_0 + 0.004003346842446*v_md - 0.004003346842446*v_md_0 + 6.2889597697231e-5*v_mq - 6.2889597697231e-5*v_mq_0 + 0.768729374614947*xD[4] + 0.0120761660206275*xD[5] - 0.0376316155873075*xD[6] - 0.000591164657503082*xD[7] - 0.060612006008949*xD[8] - 0.000952169478073937*xD[9];
    xD[5] = -0.103767816974477*Dx_r_1 + 13.7996619184135*Dx_r_2 - 0.00455866714091368*i_sd + 0.00455866714091368*i_sd_0 + 0.290189894236833*i_sq - 0.290189894236833*i_sq_0 - 6.28895976972104e-5*v_md + 6.28895976972104e-5*v_md_0 + 0.00400334684244601*v_mq - 0.00400334684244601*v_mq_0 - 0.0120761660206275*xD[4] + 0.768729374614947*xD[5] + 0.000591164657503061*xD[6] - 0.0376316155873075*xD[7] + 0.000952169478074047*xD[8] - 0.060612006008949*xD[9];
    xD[6] = 86.1438235366479*Dx_r_1 + 0.886477352734974*Dx_r_2 - 4.2224902133574*i_sd + 4.2224902133574*i_sd_0 - 0.0663321768633114*i_sq + 0.0663321768633114*i_sq_0 + 1.05808548346984*v_md - 1.05808548346984*v_md_0 + 0.016621735014091*v_mq - 0.016621735014091*v_mq_0 + 10.5088339827692*xD[4] + 0.165085956189328*xD[5] - 0.517613418057255*xD[6] - 0.00813132134321622*xD[7] - 6.28634376941182*xD[8] - 0.0987537793260168*xD[9];
    xD[7] = -0.886477352734974*Dx_r_1 + 86.1438235366479*Dx_r_2 + 0.0663321768633069*i_sd - 0.0663321768633069*i_sd_0 - 4.2224902133574*i_sq + 4.2224902133574*i_sq_0 - 0.0166217350140914*v_md + 0.0166217350140914*v_md_0 + 1.05808548346984*v_mq - 1.05808548346984*v_mq_0 - 0.165085956189328*xD[4] + 10.5088339827692*xD[5] + 0.00813132134321657*xD[6] - 0.517613418057256*xD[7] + 0.0987537793260213*xD[8] - 6.28634376941182*xD[9];
    xD[8] = 1.18794715879488*Dx_r_1 + 0.0139161908172305*Dx_r_2 + 0.506762435260992*i_sd - 0.506762435260992*i_sd_0 + 0.00796086048395717*i_sq - 0.00796086048395717*i_sq_0 + 0.0441783203908041*v_md - 0.0441783203908041*v_md_0 + 0.000694008514789877*v_mq - 0.000694008514789877*v_mq_0 + 0.229577888227884*xD[4] + 0.00360649766283964*xD[5] - 0.0105500516459425*xD[6] - 0.000165733454984027*xD[7] + 0.261966939353954*xD[8] + 0.00411530553667028*xD[9];
    xD[9] = -0.0139161908172305*Dx_r_1 + 1.18794715879488*Dx_r_2 - 0.00796086048395704*i_sd + 0.00796086048395704*i_sd_0 + 0.506762435260992*i_sq - 0.506762435260992*i_sq_0 - 0.000694008514789839*v_md + 0.000694008514789839*v_md_0 + 0.044178320390804*v_mq - 0.044178320390804*v_mq_0 - 0.00360649766283964*xD[4] + 0.229577888227884*xD[5] + 0.000165733454983989*xD[6] - 0.0105500516459425*xD[7] - 0.00411530553667041*xD[8] + 0.261966939353954*xD[9];

```python
Du_r_1,Du_r_2 = sym.symbols('Du_r_1,Du_r_2')
Du_r = sym.Matrix([Du_r_1,Du_r_2 ])

Dx_r = W@Du_r

Dx_r_1 = str(sym.N(Dx_r[0],8))
Dx_r_1 = str(sym.N(Dx_r[1],8))

print(f'Dx_r_1 = {u_r_d};')
print(f'Dx_r_2 = {u_r_q};')
```

    Dx_r_1 = -0.013361535*Dx_d_1 - 0.00027748637*Dx_d_2 + 0.0021866204*Dx_d_3 + 5.2865939e-5*Dx_d_4 + 0.0071983421*Dx_d_5 + 0.00022706741*Dx_d_6 - 2.2443754*Dx_i_1 + 0.68967688*Dx_i_2 - 0.36460717*Dx_r_1 - 0.0049366387*Dx_r_2;
    Dx_r_2 = 0.00027748637*Dx_d_1 - 0.013361535*Dx_d_2 - 5.2865939e-5*Dx_d_3 + 0.0021866204*Dx_d_4 - 0.00022706741*Dx_d_5 + 0.0071983421*Dx_d_6 - 0.68967688*Dx_i_1 - 2.2443754*Dx_i_2 + 0.0049366387*Dx_r_1 - 0.36460717*Dx_r_2;

```python
for it in range(6):
    print(f'xD[{it+it_ini}] = {Δx_o_kp1[it]};')
```

    xD[4] = 13.7996619184135*Dx_r_1 + 0.103767816974477*Dx_r_2 + 0.290189894236834*i_sd - 0.290189894236834*i_sd_0 + 0.00455866714091358*i_sq - 0.00455866714091358*i_sq_0 + 0.004003346842446*v_md - 0.004003346842446*v_md_0 + 6.2889597697231e-5*v_mq - 6.2889597697231e-5*v_mq_0 + 0.768729374614947*xD[4] + 0.0120761660206275*xD[5] - 0.0376316155873075*xD[6] - 0.000591164657503082*xD[7] - 0.060612006008949*xD[8] - 0.000952169478073937*xD[9];
    xD[5] = -0.103767816974477*Dx_r_1 + 13.7996619184135*Dx_r_2 - 0.00455866714091368*i_sd + 0.00455866714091368*i_sd_0 + 0.290189894236833*i_sq - 0.290189894236833*i_sq_0 - 6.28895976972104e-5*v_md + 6.28895976972104e-5*v_md_0 + 0.00400334684244601*v_mq - 0.00400334684244601*v_mq_0 - 0.0120761660206275*xD[4] + 0.768729374614947*xD[5] + 0.000591164657503061*xD[6] - 0.0376316155873075*xD[7] + 0.000952169478074047*xD[8] - 0.060612006008949*xD[9];
    xD[6] = 86.1438235366479*Dx_r_1 + 0.886477352734974*Dx_r_2 - 4.2224902133574*i_sd + 4.2224902133574*i_sd_0 - 0.0663321768633114*i_sq + 0.0663321768633114*i_sq_0 + 1.05808548346984*v_md - 1.05808548346984*v_md_0 + 0.016621735014091*v_mq - 0.016621735014091*v_mq_0 + 10.5088339827692*xD[4] + 0.165085956189328*xD[5] - 0.517613418057255*xD[6] - 0.00813132134321622*xD[7] - 6.28634376941182*xD[8] - 0.0987537793260168*xD[9];
    xD[7] = -0.886477352734974*Dx_r_1 + 86.1438235366479*Dx_r_2 + 0.0663321768633069*i_sd - 0.0663321768633069*i_sd_0 - 4.2224902133574*i_sq + 4.2224902133574*i_sq_0 - 0.0166217350140914*v_md + 0.0166217350140914*v_md_0 + 1.05808548346984*v_mq - 1.05808548346984*v_mq_0 - 0.165085956189328*xD[4] + 10.5088339827692*xD[5] + 0.00813132134321657*xD[6] - 0.517613418057256*xD[7] + 0.0987537793260213*xD[8] - 6.28634376941182*xD[9];
    xD[8] = 1.18794715879488*Dx_r_1 + 0.0139161908172305*Dx_r_2 + 0.506762435260992*i_sd - 0.506762435260992*i_sd_0 + 0.00796086048395717*i_sq - 0.00796086048395717*i_sq_0 + 0.0441783203908041*v_md - 0.0441783203908041*v_md_0 + 0.000694008514789877*v_mq - 0.000694008514789877*v_mq_0 + 0.229577888227884*xD[4] + 0.00360649766283964*xD[5] - 0.0105500516459425*xD[6] - 0.000165733454984027*xD[7] + 0.261966939353954*xD[8] + 0.00411530553667028*xD[9];
    xD[9] = -0.0139161908172305*Dx_r_1 + 1.18794715879488*Dx_r_2 - 0.00796086048395704*i_sd + 0.00796086048395704*i_sd_0 + 0.506762435260992*i_sq - 0.506762435260992*i_sq_0 - 0.000694008514789839*v_md + 0.000694008514789839*v_md_0 + 0.044178320390804*v_mq - 0.044178320390804*v_mq_0 - 0.00360649766283964*xD[4] + 0.229577888227884*xD[5] + 0.000165733454983989*xD[6] - 0.0105500516459425*xD[7] - 0.00411530553667041*xD[8] + 0.261966939353954*xD[9];

```python
z_obs_list
```

    ['v_md_g01', 'v_mq_g01', 'i_sd_g01', 'i_sq_g01']

```python
K_o
```

    matrix([[ 2.20758392e-02,  3.46794994e-04,  2.30800093e-01,
              3.62569759e-03],
            [-3.46794994e-04,  2.20758392e-02, -3.62569759e-03,
              2.30800093e-01],
            [ 1.30196616e+00,  2.04529188e-02, -1.04919893e+01,
             -1.64821339e-01],
            [-2.04529188e-02,  1.30196616e+00,  1.64821339e-01,
             -1.04919893e+01],
            [ 5.02642205e-02,  7.89613473e-04,  7.69087392e-01,
              1.20817902e-02],
            [-7.89613473e-04,  5.02642205e-02, -1.20817902e-02,
              7.69087392e-01]])

```python
import sympy as sym

x_d_1,x_d_2,x_d_3,x_d_4,x_d_5,x_d_6 = sym.symbols('xD[4],xD[5],xD[6],xD[7],xD[8],xD[9]')
x_r_1,x_r_2 = sym.symbols('Dx_r_1,Dx_r_2')
x_i_1,x_i_2 = sym.symbols('Dx_i_1,Dx_i_2')

x_e = sym.Matrix([x_d_1,x_d_2,x_d_3,x_d_4,x_d_5,x_d_6,x_r_1,x_r_2,x_i_1,x_i_2])
u_r = -K_c * x_e

u_r_d = str(sym.N(u_r[0],8))
u_r_q = str(sym.N(u_r[1],8))

print(f'Du_r_1 = {u_r_d};')
print(f'Du_r_2 = {u_r_q};')
print('\nWarning: Control output is eta_dq!!')
```

    Du_r_1 = -2.2443754*Dx_i_1 + 0.68967688*Dx_i_2 - 0.36460717*Dx_r_1 - 0.0049366387*Dx_r_2 - 0.013361535*xD[4] - 0.00027748637*xD[5] + 0.0021866204*xD[6] + 5.2865939e-5*xD[7] + 0.0071983421*xD[8] + 0.00022706741*xD[9];
    Du_r_2 = -0.68967688*Dx_i_1 - 2.2443754*Dx_i_2 + 0.0049366387*Dx_r_1 - 0.36460717*Dx_r_2 + 0.00027748637*xD[4] - 0.013361535*xD[5] - 5.2865939e-5*xD[6] + 0.0021866204*xD[7] - 0.00022706741*xD[8] + 0.0071983421*xD[9];

    Warning: Control output is eta_dq!!

```python
B_o
```

    array([[ 1.37996619e+01,  1.03767817e-01],
           [-1.03767817e-01,  1.37996619e+01],
           [ 8.61438235e+01,  8.86477353e-01],
           [-8.86477353e-01,  8.61438235e+01],
           [ 1.18794716e+00,  1.39161908e-02],
           [-1.39161908e-02,  1.18794716e+00]])

```python

```


# Numerické řešení konvekce s difusí pomocí MKO

## Síť pro MKO

```julia
using Plots
using StaticArrays
import Base.-, Base.+, Base.*, Base./
using ForwardDiff
using LinearAlgebra

pyplot();
```

```julia
const Scalar = Float64;
const Vector = SVector{2,Float64};
const Label  = Int;
```

```julia
×(u::Vector, v::Vector) = u[1]*v[2] - u[2]*v[1];
```

```julia
const ScalarList = Array{Scalar,1};
const VectorList = Array{Vector,1};
const LabelList  = Array{Label,1};
const LabelListList = Array{LabelList,1};
```

```julia
struct PatchInfo
    name  :: String
    range :: UnitRange{Label}
end;

const PatchInfoList = Array{PatchInfo,1};
```

```julia
struct Mesh
    point      :: VectorList     # Souradnice vrcholu site
    centre     :: VectorList     # Souradnice stredu bunek
    volume     :: ScalarList     # Velikosti (objemy) bunek
    surface    :: VectorList     # Normalovy vektor na stenu
    facecentre :: VectorList     # Souradnice stredu strany
    owner      :: LabelList      # Vlastnik steny
    neighbor   :: LabelList      # Soused steny
    patch      :: PatchInfoList  # Seznam okrajovych podminek

    cell2point :: LabelListList  # Indexy vrcholu bunek
    face2point :: LabelListList  # Indexy vrcholu sten
end

"""
    Mesh(point, owner, neighbor, patch, cell2point, face2point)

    Vytvori sit ze zadanych souradnic vrcholu a informaci o konektivite
"""
function Mesh(point::VectorList, owner::LabelList, neighbor::LabelList, patch::PatchInfoList, cell2point::LabelListList, face2point::LabelListList)
    m = Mesh(point, VectorList(), ScalarList(), VectorList(), VectorList(), owner, neighbor, patch, cell2point, face2point)
    update!(m)
    return m
end
```

    Mesh

```julia
"""
    update!(mesh::Mesh)

    Vypocte objemy bunek, souradnice stredu bunek, normalove vektory na steny a stredy sten.
"""
function update!(mesh::Mesh)

    # Recalculate cell centres & volumes
    ncells = size(mesh.cell2point,1)
    empty!(mesh.centre)
    empty!(mesh.volume)
    for c=1:ncells
        pts = mesh.point[mesh.cell2point[c]]

        v = Scalar(0)
        x = Vector(0,0)
        p1 = pts[end]
        for i=1:size(pts,1)-2
            p2 = pts[i]
            p3 = pts[i+1]
            vv = (p2 - p1) × (p3 - p1) / 2
            v += vv
            x += vv*(p1 + p2 + p3)/3
        end
        push!(mesh.volume, v)
        push!(mesh.centre, x/v)
    end

    # Recalculate face sizes & positions
    empty!(mesh.surface)
    empty!(mesh.facecentre)
    nfaces = size(mesh.face2point,1)
    for f=1:nfaces
        pts = mesh.point[mesh.face2point[f]]
        push!(mesh.surface, Vector(pts[2][2]-pts[1][2], pts[1][1]-pts[2][1]))
        push!(mesh.facecentre, (pts[1]+pts[2])/2)
    end
end
```

    update!

```julia
"""
    cartesian_mesh(nx, ny)

    Vytvori kartezskou sit o nx*ny bunkach v jednotkovem ctverci. Okrajove podminky pojmenuje "left", "right", "top" a "bottom"
"""
function cartesian_mesh(nx, ny)
    Δx, Δy = 1.0/nx, 1.0/ny

    pid(i,j) = i + j*(nx+1) + 1
    cid(i,j) = i + (j-1)*nx

    # Mesh points
    point = VectorList()
    for j=0:ny, i=0:nx
        push!(point, Vector(i*Δx, j*Δy))
    end

    # Mesh cells
    cell2point = LabelListList()
    for j=1:ny, i=1:nx
        pts = [pid(i-1,j-1), pid(i,j-1), pid(i,j), pid(i-1,j)] |> LabelList
        push!(cell2point, pts)
    end

    # Mesh faces
    owner = LabelList()
    neighbor = LabelList()
    face2point = LabelListList()

    # Internal faces
    for j=1:ny, i=1:nx-1
        push!(owner, cid(i,j))
        push!(neighbor, cid(i+1,j))
        push!(face2point, [pid(i,j-1), pid(i,j)])
    end

    for j=1:ny-1, i=1:nx
        push!(owner, cid(i,j))
        push!(neighbor, cid(i,j+1))
        push!(face2point, [pid(i,j), pid(i-1,j)])
    end

    # Boundary patches
    # - bottom
    patch = PatchInfoList()
    j=0
    start = size(owner,1)+1
    for i=1:nx
        push!(owner, cid(i,j+1))
        push!(face2point, [pid(i-1,j), pid(i,j)])
    end
    push!(patch, PatchInfo("bottom", UnitRange(start, size(owner,1))))

    # - right
    i=nx
    start = size(owner,1)+1
    for j=1:ny
        push!(owner, cid(i,j))
        push!(face2point, [pid(i,j-1), pid(i,j)])
    end
    push!(patch, PatchInfo("right", UnitRange(start, size(owner,1))))

    # - top
    j=ny
    start = size(owner,1)+1
    for i=1:nx
        push!(owner, cid(i,j))
        push!(face2point, [pid(i,j), pid(i-1,j)])
    end
    push!(patch, PatchInfo("top", UnitRange(start, size(owner,1))))

    # - left
    i=0
    start = size(owner,1)+1
    for j=1:ny
        push!(owner, cid(i+1,j))
        push!(face2point, [pid(i,j), pid(i,j-1)])
    end
    push!(patch, PatchInfo("left", UnitRange(start, size(owner,1))))

    return Mesh(point, owner, neighbor, patch, cell2point, face2point)
end
```

    cartesian_mesh

```julia
cells(m::Mesh) = UnitRange(1,length(m.volume))

internal_faces(m::Mesh) = UnitRange(1,length(m.neighbor))

boundary_patches(m::Mesh) = UnitRange(1,length(m.patch))

patch_names(m::Mesh) = [ m.patch[p].name for p in boundary_patches(m)]

patch_by_name(m::Mesh, name::String) = findfirst(x->x==name, patch_names(m))

patch_faces(m::Mesh, patch::Label) = m.patch[patch].range
```

    patch_faces (generic function with 1 method)

```julia
m2 = cartesian_mesh(2,2);
```

```julia
for p in m2.point
    println("p = ", p)
end
```

    p = [0.0, 0.0]
    p = [0.5, 0.0]
    p = [1.0, 0.0]
    p = [0.0, 0.5]
    p = [0.5, 0.5]
    p = [1.0, 0.5]
    p = [0.0, 1.0]
    p = [0.5, 1.0]
    p = [1.0, 1.0]

```julia
for c in m2.centre
    println("c = ", c)
end

print("volume[:] = ", m2.volume)
```

    c = [0.25, 0.25]
    c = [0.75, 0.25]
    c = [0.25, 0.75]
    c = [0.75, 0.75]
    volume[:] = [0.25, 0.25, 0.25, 0.25]

```julia
for f in internal_faces(m2)
    println("S=", m2.surface[f], "\t o=", m2.owner[f], "\t n=",m2.neighbor[f])
end
```

    S=[0.5, 0.0]	 o=1	 n=2
    S=[0.5, 0.0]	 o=3	 n=4
    S=[0.0, 0.5]	 o=1	 n=3
    S=[0.0, 0.5]	 o=2	 n=4

```julia
for p in boundary_patches(m2)
    println("PATCH ", m2.patch[p].name)
    for f in patch_faces(m2, p)
        println("S=", m2.surface[f], "\t o=", m2.owner[f])
    end
    println()
end
```

    PATCH bottom
    S=[0.0, -0.5]	 o=1
    S=[0.0, -0.5]	 o=2

    PATCH right
    S=[0.5, 0.0]	 o=2
    S=[0.5, 0.0]	 o=4

    PATCH top
    S=[0.0, 0.5]	 o=3
    S=[0.0, 0.5]	 o=4

    PATCH left
    S=[-0.5, 0.0]	 o=1
    S=[-0.5, 0.0]	 o=3

## Uložení dat a okrajových podmínek

K uložení dat použijeme speciální datovou strukturu `Field`, která zahrnuje jak hodnoty řešení ve středech buněk, tak odkaz (ukazatel) na síť a seznam okrajových podmínek.

```julia
mutable struct Field
    values
    mesh
    boundaries
end

Field(m::Mesh) = Field( zeros(length(m.centre)), m, Dict{String,Any}());
Field(values, m::Mesh) = Field(values, m, Dict{String,Any}());
```

### Operace se strukturou `Field`

Se strukturou `Field` budeme provádět aritmetické operace (sčítat, odčítat, násobit, dělit, ...) prvek po prvku

```julia
for op ∈ [:+, :-, :*, :/]
    @eval begin
        function ($op)(a::Field, b::Field)
            @assert a.mesh == b.mesh
            Field(broadcast($op,a.values,b.values), a.mesh)
        end
    end
end
```

```julia
a2 = Field(m2); a2.values = ones(4);
b2 = Field(m2); b2.values = 3*ones(4);
```

```julia
c2 = a2 * b2;
c2.values
```

    4-element Array{Float64,1}:
     3.0
     3.0
     3.0
     3.0

Umíme k ní přičíst konstantu ke každému prvku, umíme přičíst pole správné velikosti a umíme jí vynásobit či vydělit konstantou. Dále umíme unární mínus.

```julia
for op ∈ [:+, :-, :*, :/]
    @eval begin
        function ($op)(a::Field, b::Number)
            Field(broadcast($op,a.values,b), a.mesh)
        end
    end
end

for op ∈ [:+, :-, :*, :/]
    @eval begin
        function ($op)(a::Field, b::Array{Float64,1})
            @assert size(a.values) == size(b)
            Field(broadcast($op,a.values,b), a.mesh)
        end
    end
end

-(a::Field) = Field(-a.values, a.mesh);
```

```julia
c2 = a2 / 3;
c2.values
```

    4-element Array{Float64,1}:
     0.3333333333333333
     0.3333333333333333
     0.3333333333333333
     0.3333333333333333

### Přiřazení do `Field`

Zde je třeba trochu opatrnosti. Při prostém přiřazení dojde v jazyce Julia pouze k nastavení ukazatele na hodnotu na pravé straně. To ale mimo jiné vymaže odkaz na okrajové podmínky. Proto raději definujeme oprátor <- který pouze kopíruje položku values.

```julia
function ←(a::Field, b::Field)
    @assert a.mesh == b.mesh
    a.values = copy(b.values)
end
```

    ← (generic function with 1 method)

```julia
function ←(a::Field, val::Float64)
    for i in eachindex(a.values)
        a.values[i] = val
    end
end
```

    ← (generic function with 2 methods)

```julia
b2 = Field(m2);
b2 ← 3.0;
c2 ← b2;
d2 = b2;
b2.values[1] = 5;
println(c2.values)
println(d2.values)
```

    [3.0, 3.0, 3.0, 3.0]
    [5.0, 3.0, 3.0, 3.0]

### Okrajová podmínka

Pro metodu konečných objemů potřebujeme umět vyčíslit hodnotu řešení a případně gradientu řešení na hranici. Jednou z možností je implementovat podminku tak, že
$$
    u_b = c_1 + c_2 u_1,
$$
kde $u_b$ je hodnota na hranici, $u_1$ je hodnota ve středu první buňky a $c_1$ a $c_2$ jsou koeficienty.

```julia
struct BoundaryCondition
    c1    :: ScalarList
    c2    :: ScalarList
    field :: Field
    patch :: Label
end
```

Dirichletova podmínka je potom implementována tak, že $c_1$ je předepsaná hodnota a $c_2=0$.

```julia
function boundary_coeffs(bc::BoundaryCondition, f::Label)
    field = bc.field
    mesh = field.mesh
    start = first(mesh.patch[bc.patch].range)
    i = f + 1 - start

    c1 = bc.c1[i]
    c2 = bc.c2[i]

    return c1, c2
end
```

    boundary_coeffs (generic function with 1 method)

```julia
function boundary_value(bc::BoundaryCondition, f::Label)
    field = bc.field
    mesh = field.mesh
    u1 = field.values[mesh.owner[f]]
    c1, c2 = boundary_coeffs(bc, f)
    return c1 + c2*u1
end
```

    boundary_value (generic function with 1 method)

```julia
function set_dirichlet_patch!(field::Field, name::String, val::Scalar)
    mesh = field.mesh
    p = patch_by_name(mesh, name)
    c1 = [val for f in patch_faces(mesh, p)]
    c2 = zero(c1)
    field.boundaries[name] = BoundaryCondition(c1, c2, field, p)
end;
```

Neumannova podmínka je implementována jako $c_1=0$ a $c_2=1$. Pro případ zadané nenulové normálvé derivace je výpočet následující:
$$
\frac{u_b-u_1}{\delta} = \frac{\partial u}{\partial n},
$$
kde
$$
    \delta = (\vec{x}_f - \vec{x}_o)\cdot \vec{n}.
$$
$\vec{x}_f$ je střed stěny, $\vec{x}_o$ je střed buňky. Tzn.
$$
    u_b = \delta \frac{\partial u}{\partial n} + u_1.
$$

```julia
function set_neumann_patch!(field::Field, name::String)
    mesh = field.mesh
    p = patch_by_name(mesh, name)
    c2 = [1.0 for f in patch_faces(mesh, p)]
    c1 = zero(c2)
    field.boundaries[name] = BoundaryCondition(c1, c2, field, p)
end;
```

```julia
m3 = cartesian_mesh(3,3);
```

```julia
u3 = Field(m3);
u3 ← 0.0;
set_dirichlet_patch!(u3, "left", 1.0);
set_dirichlet_patch!(u3, "bottom", 0.0);
set_neumann_patch!(u3, "right");
set_neumann_patch!(u3, "top");
```

## Výpočet pomocí MKO

Řešíme rovnici

\begin{equation}
    \frac{\partial u}{\partial t} + \nabla\cdot(\vec{v} u) = 0
\end{equation}
s $\vec{v}=[1,0.5]$, počáteční podmínkou $u(0,\vec{x}) = 0$ a s okrajovými podmínkami $u=0$ pro $x=0$ a $u=1$ pro $y=0$.

Výpočet provedeme pomocí MKO v explicitním tvaru, toky přes hranice budeme aproximovat pomocí metody *upwind*. Tedy
$$
    u_i^{n+1} = u_i^n - \frac{\Delta t}{|\Omega_i|} \sum_{j} H_f(u_i^n, u_j^n, \vec{S}_{ij}),
$$
kde $j$ probíhá přes sousedy buňky $i$.

Sumy na pravé straně vyčíslíme dle následujícího algoritmu:
1. Vynulujeme $r_i:=0$
2. Pro všechny vnitřní stěny vypočteme tok z buňky *owner* do *neighbour*, ten pak přidáme k $r[owner]$ a odebereme od $r[neighbour]$ (pozn. pozor na znaménko!).
3. Podobně projdeme všechny hraniční stěny, zde není buňka *neighbour* a výpočet toku musíme provést dle zadané okrajové podmíny.
4. Nakonec celé pole dělíme objemy buněk.

```julia
v = Vector(1, 0.5);

# Tok typu upwind
function H(ul, ur, s)
    ϕ = dot(v,s)
    if ϕ > 0
        return ϕ*ul
    else
        return ϕ*ur
    end
end
```

    H (generic function with 1 method)

```julia
function R(u::Field)
    mesh = u.mesh
    r = zero(u.values)

    for f in internal_faces(mesh)
        o, n = mesh.owner[f], mesh.neighbor[f]
        S = mesh.surface[f]
        flux = H(u.values[o], u.values[n], S)
        r[o] += flux
        r[n] -= flux
    end

    for p in boundary_patches(mesh)
        name = mesh.patch[p].name
        bc = u.boundaries[name]

        for f in patch_faces(mesh, p)
            o = mesh.owner[f]
            S = mesh.surface[f]
            ub = boundary_value(bc, f)
            flux = H(u.values[o], ub, S)
            r[o] += flux
        end
    end

    r ./= mesh.volume

    return r
end
```

    R (generic function with 1 method)

```julia
R(u3)
```

    9-element Array{Float64,1}:
     -3.0
      0.0
      0.0
     -3.0
      0.0
      0.0
     -3.0
      0.0
      0.0

```julia
m = cartesian_mesh(50,50);

u = Field(m)
u ← 0.0;
set_dirichlet_patch!(u, "left", 1.0);
set_dirichlet_patch!(u, "bottom", 0.0);
set_neumann_patch!(u, "right");
set_neumann_patch!(u, "top");
```

```julia
Δt = 0.01
for t = 0 : Δt : 0.5
    u ← u - Δt * R(u)
end
```

```julia
plot(reshape(u.values, 50,50), st=:contour, color=:lightrainbow, fill=true, aspect_ratio=:equal)
```

## Implicitní metoda

Implicitní metoda je výpočetně náročnější (v každém kroku řešíme soustavu rovnic), nicméně často bývá efektivnější neboť umožňuje použití delšího časového kroku. Pokusíme se naprogramovat implicitní metodu pro řešení **lineární** parciální diferenciální rovnice tak, že budeme jednotlivé operátory (derivace) reprezentovat jako trojici matice, neznámá a absolutní člen, tj.
$$
    L(x) = A x + b.
$$

```julia
using SparseArrays;
```

```julia
struct Equation
    A
    x
    b
end
```

S rovnicemi umíme provádět běžné operace

```julia
# Scitani a odcitani rovnic
for op ∈ [:+, :-]
    @eval begin
        function ($op)(e1::Equation, e2::Equation)
            @assert e1.x == e2.x
            Equation(($op)(e1.A, e2.A), e1.x, $(op)(e1.b, e2.b))
        end
    end
end

# Pricteni/odecteni pole o spravne velikosti
for op ∈ [:+, :-]
    @eval begin
        function ($op)(e1::Equation, f::Array{Float64,1})
            @assert size(e1.x) == size(f)
            Equation(copy(e1.A), e1.x, $(op)(e1.b, f))
        end
    end
end

# Vynasobeni rovnice konstantou
*(a::Float64, eq::Equation) = Equation(a*eq.A, eq.x, a*eq.b);
```

Rovnice umíme vyřešit

```julia
function solve!(eqn::Equation)
    x = eqn.A \ eqn.b
    for i in eachindex(eqn.x)
        eqn.x[i] = -x[i]
    end
end;
```

### Operátor $\partial/\partial t$

Časovou derivaci aproximujeme jako
$$
    \frac{\partial u}{\partial t} \approx \frac{u^{n+1}-u^{n}}{\Delta t}
$$
tj. jako
$$
T u^{n+1} + b
$$
kde $T=diag({1/\Delta t})$ a $b_i=- u_i^n/\Delta t$.

```julia
function ddt(f::Field,Δt)
    n = length(f.values)
    Equation(I/Δt, f.values, -f.values/Δt)
end;
```

```julia
ddt(u3,0.1)
```

    Equation(UniformScaling{Float64}(10.0), [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [-0.0, -0.0, -0.0, -0.0, -0.0, -0.0, -0.0, -0.0, -0.0])

Zkusíme vyřešit soustavu ODE $\dot{u} = 1$ s počáteční podmínkou $u(0)=0$. Uděláme 10 kroků s $\Delta t=0.1$.

```julia
u3 ← 0.0
for t=1:10
    solve!(ddt(u3,0.1)-ones(9))
    print(u3.values[1], "\t")
end
```

    0.1	0.2	0.3	0.39999999999999997	0.5	0.6	0.7	0.7999999999999999	0.9	1.0

## Operátor $div(\vec{v}u)$

Operátor je reprezentován řídkou maticí a absolutním členem $b$. Matici sestavíme stejným způsobem jako při výpočtu $r_i$ explicitní metodou.

```julia
function div(v::Vector, u::Field)
    A = spzeros(length(u.values),length(u.values))
    b = zero(u.values)

    msh = u.mesh

    for f in internal_faces(msh)
        o = msh.owner[f]
        n = msh.neighbor[f]
        S = msh.surface[f]
        uo = u.values[o]
        un = u.values[n]

        ϕ = dot(v,S)
        α = max(ϕ, 0.0)
        β = min(ϕ, 0.0)

        A[o,o] += α / msh.volume[o]
        A[o,n] += β / msh.volume[o]

        A[n,o] -= α / msh.volume[n]
        A[n,n] -= β / msh.volume[n]
    end

    for p in boundary_patches(msh)
        name = msh.patch[p].name
        bc = u.boundaries[name]

        for f in patch_faces(msh, p)
            o = msh.owner[f]
            S = msh.surface[f]

            ϕ = dot(v,S)
            α = max(ϕ, 0.0)
            β = min(ϕ, 0.0)

            # ub = c1 + c2*u[o], flux=alpha*u[o] + beta*ub
            c1, c2 = boundary_coeffs(bc, f)

            A[o,o] += (α + β*c2) / msh.volume[o]
            b[o]   += β*c1 / msh.volume[o]
        end
    end

    return Equation(A, u.values, b)
end
```

    div (generic function with 1 method)

```julia
Matrix(div(v,u3).A)
```

    9×9 Array{Float64,2}:
      4.5   0.0   0.0   0.0   0.0   0.0   0.0   0.0  0.0
     -3.0   4.5   0.0   0.0   0.0   0.0   0.0   0.0  0.0
      0.0  -3.0   4.5   0.0   0.0   0.0   0.0   0.0  0.0
     -1.5   0.0   0.0   4.5   0.0   0.0   0.0   0.0  0.0
      0.0  -1.5   0.0  -3.0   4.5   0.0   0.0   0.0  0.0
      0.0   0.0  -1.5   0.0  -3.0   4.5   0.0   0.0  0.0
      0.0   0.0   0.0  -1.5   0.0   0.0   4.5   0.0  0.0
      0.0   0.0   0.0   0.0  -1.5   0.0  -3.0   4.5  0.0
      0.0   0.0   0.0   0.0   0.0  -1.5   0.0  -3.0  4.5

### Vlastnosti matice $C$ reprezentující operátor $div(\vec{v}u)$

Nechť C je matice získaná reprezentující lineární operátor $div(\vec{v}u)$ získaná pomocí MKO a schématu **upwind**. Potom:
1. C je (neostře) diagonálně dominantní,
2. $C_{ii}>0$ a $C_{ij} \le 0$ pro $i\ne j$.
3. V řádcích odpovídajících vnitřním buňkám sítě (tj. ty, které nesousedí s žádnou hranicí) je $\sum_j C_{ij}=0$.

Vyřešíme rovnici konvekce do času $t=0.5s$ pomocí implicitní metody s krokem $\Delta t=0.05$

```julia
u ← 0.0

Δt = 0.05
for t=0:Δt:0.5
    solve!( ddt(u,Δt) + div(v,u) );
end
```

```julia
plot(reshape(u.values, 50,50), st=:contour, color=:lightrainbow, fill=true, aspect_ratio=:equal)
```

### Operátor $\Delta u$

Operátor $\Delta u$ je rozepsán jako $\Delta u = \nabla \cdot (\nabla u)$. Diskretizace se provede podobně jako u konvektivního členu. Tok $\nabla u \cdot \vec{S}$ je aproximován centrálně, tj.
$$
    \nabla u \cdot \vec{S} \approx ||\vec{S}|| \frac{u_n - u_o}{||\vec{x}_n-\vec{x}_o||}
$$
Zde se dopouštíme zjednodušení kdy předpokládáme, že vektor $\vec{S}$ je rovnoběžný s $\vec{x}_n - \vec{x}_o$ (ortogonalita sítě).

```julia
function Δ(μ::Float64, u::Field)
    mesh = u.mesh

    A = spzeros(length(u.values),length(u.values))
    b = zero(u.values)

    for f in internal_faces(mesh)
        o, n = mesh.owner[f], mesh.neighbor[f]
        xo, xn = mesh.centre[o], mesh.centre[n]
        S = mesh.surface[f]

        g = μ * norm(S) / norm(xn - xo)

        A[o,o] -= g / mesh.volume[o]
        A[o,n] += g / mesh.volume[o]

        A[n,o] += g / mesh.volume[n]
        A[n,n] -= g / mesh.volume[n]
    end

    for p in boundary_patches(mesh)
        name = mesh.patch[p].name
        bc = u.boundaries[name]

        for f in patch_faces(mesh, p)
            o = mesh.owner[f]
            S = mesh.surface[f]
            xo, xf = mesh.centre[o], mesh.facecentre[f]
            c1, c2 = boundary_coeffs(bc, f)

            # normal derivative at boundary dudn = (ub - uin) / delta
            # with ub = c[1] + c[2]*uin, i.e.
            # dudn = c[1]/delta + (c[2]-1)/delta*uin

            δ = dot(xf - xo, S)/norm(S)
            A[o,o] += μ * (c2-1)/δ * norm(S) / mesh.volume[o]
            b[o] += μ * c1/δ * norm(S) / mesh.volume[o]
        end
    end

    Equation(A, u.values, b)
end

```

    Δ (generic function with 1 method)

### Vlastnosti matice $D$ reprezentující operátor $\Delta$

1. Matice $D$ je symetrická
2. Matice $D$ je neostře diagonálně dominantní
3. $D_{ii}<0$, $D_{ij} \ge 0$ pro $i\ne j$
4. Matice $-D$ je pozitivně definitní

```julia
D=Matrix(Δ(1.0,u3).A)
```

    9×9 Array{Float64,2}:
     -54.0    9.0    0.0    9.0    0.0    0.0    0.0    0.0    0.0
       9.0  -45.0    9.0    0.0    9.0    0.0    0.0    0.0    0.0
       0.0    9.0  -36.0    0.0    0.0    9.0    0.0    0.0    0.0
       9.0    0.0    0.0  -45.0    9.0    0.0    9.0    0.0    0.0
       0.0    9.0    0.0    9.0  -36.0    9.0    0.0    9.0    0.0
       0.0    0.0    9.0    0.0    9.0  -27.0    0.0    0.0    9.0
       0.0    0.0    0.0    9.0    0.0    0.0  -36.0    9.0    0.0
       0.0    0.0    0.0    0.0    9.0    0.0    9.0  -27.0    9.0
       0.0    0.0    0.0    0.0    0.0    9.0    0.0    9.0  -18.0

## Řešení rovnice konvekce s difuzí pomocí MKO

V každém kroku se řeší soustava rovnic s maticí
$A = T + C - D$. Tato matice je:
1. nesymetrická,
2. ODD,
3. $A_{ii}>0$, $A_{ij} \le 0$ pro $i \ne j$,
4. $\sum_j A_{ij} \le 1/\Delta t$.

```julia
A = Matrix((ddt(u3,Δt) + div(v,u3) - Δ(1.e-1,u3)).A)
```

    9×9 Array{Float64,2}:
     29.9  -0.9   0.0  -0.9   0.0   0.0   0.0   0.0   0.0
     -3.9  29.0  -0.9   0.0  -0.9   0.0   0.0   0.0   0.0
      0.0  -3.9  28.1   0.0   0.0  -0.9   0.0   0.0   0.0
     -2.4   0.0   0.0  29.0  -0.9   0.0  -0.9   0.0   0.0
      0.0  -2.4   0.0  -3.9  28.1  -0.9   0.0  -0.9   0.0
      0.0   0.0  -2.4   0.0  -3.9  27.2   0.0   0.0  -0.9
      0.0   0.0   0.0  -2.4   0.0   0.0  28.1  -0.9   0.0
      0.0   0.0   0.0   0.0  -2.4   0.0  -3.9  27.2  -0.9
      0.0   0.0   0.0   0.0   0.0  -2.4   0.0  -3.9  26.3

Řešení dané úlohy pro čas $t=0.5s$ pomocí implicitní metody s krokem $\Delta t = 0.1s$ s nulovou počáteční podmínkou.

```julia
u ← 0.0

Δt = 0.1
for t=0:Δt:0.5
    solve!( ddt(u,Δt) + div(v,u) - Δ(1.e-2,u) );
end
```

```julia
plot(reshape(u.values, 50,50), st=:contour, color=:lightrainbow, fill=true, aspect_ratio=:equal)
```

```julia

```


# Isochrons
|Author  | Stanley A. Baronett|
|--------|--------------------|
|Created | 2/23/2022          |
|Updated | 2/24/2022          |

## [Pape et al. (2019)](https://doi.org/10.1016/j.gca.2018.10.017)

> Fig. 3. The impact of a poor quality sample surface (i.e. cracks) and high-$\mathrm{Ca}$ pyroxene dendrites on SIMS analysis exemplified with sample NWA8276_Ch9 (a).
  Using all SIMS measurements i.e. including spot #2 (c) measured in highly fractured mesostasis for the isochron regression, the resulting isochron (grey solid line) is biased towards lower $(^{26}\mathrm{Al}/^{27}\mathrm{Al})_0$ of $[(]2.9 \pm 4.6[)] \times 10^{-6}$, has a large error (error envelope is not shown for the sake of clarity) and a high [MSWD] of 11.6.
  The correct isochron (red solid line) includes only measurements with appropriate sample surface (e.g. spot #1 (b)).
  Spot #3 contained very minor amounts of high-$\mathrm{Ca}$ pyroxene dendrites, but this does not bias the isochron regression, because excluding spot #3 changes the isochron only slightly towards higher $(^{26}\mathrm{Al}/^{27}\mathrm{Al})_0$ (black dotted line, within the error of the original isochron) and would shift the relative age ($\Delta t_\mathrm{CAI}$) by less than 80,000 yrs. ol: olivine; mes: mesostasis.

```python
import numpy as np
import pandas as pd

samples    = np.arange(1, 9)
phases    = ['mesostasis', 'mesostasis', 'mesostasis', 'mesostasis',
            'olivine',    'olivine',    'olivine',    'olivine']
Al_Mgs   = np.asarray([5.40, 6.20, 5.50, 4.30, 1.8e-3, 2.9e-4, 3.0e-4, 3.0e-4])
deltaMgs = np.asarray([0.28, 0.24, 0.28, 0.12,   0.00,   0.00,   0.01,   0.01])
sigma2s  = np.asarray([0.10, 0.08, 0.09, 0.07,   0.06,   0.05,   0.05,   0.05])

df = pd.DataFrame({
    'Sample'     : samples,
    'Phase'      : phases,
    '²⁷Al/²⁴Mg'  : Al_Mgs,
    'δ²⁶Mg* (‰)' : deltaMgs,
    '2σ'         : sigma2s,})
df.style.format({
    'Sample'     : 'NWA 8276 Ch9/{:d}',
    '²⁷Al/²⁴Mg'  : '{:.2e}',
    'δ²⁶Mg* (‰)' : '{:.2f}',
    '2σ'         : '{:.2f}',})
```

      Sample
      Phase
      ²⁷Al/²⁴Mg
      δ²⁶Mg* (‰)
      2σ

      0
      NWA 8276 Ch9/1
      mesostasis
      5.40e+00
      0.28
      0.10

      1
      NWA 8276 Ch9/2
      mesostasis
      6.20e+00
      0.24
      0.08

      2
      NWA 8276 Ch9/3
      mesostasis
      5.50e+00
      0.28
      0.09

      3
      NWA 8276 Ch9/4
      mesostasis
      4.30e+00
      0.12
      0.07

      4
      NWA 8276 Ch9/5
      olivine
      1.80e-03
      0.00
      0.06

      5
      NWA 8276 Ch9/6
      olivine
      2.90e-04
      0.00
      0.05

      6
      NWA 8276 Ch9/7
      olivine
      3.00e-04
      0.01
      0.05

      7
      NWA 8276 Ch9/8
      olivine
      3.00e-04
      0.01
      0.05

\begin{align*}
  N_{^xA}&=N_{^xA0}e^{-\lambda t} \\
  \ln\left(\frac{N_{^xA}}{N_{^xA0}}\right)&=-\lambda t \\
  \Delta t&=\frac{\ln(N_{^xA}/N_{^xA0})}{\lambda}
\end{align*}
where the initial $N_{^xA}=\left(^{26}\mathrm{Al}/^{27}\mathrm{Al}\right)_0$ is the slope of the linear regression, the canonical $N_{^xA0}={^{26}\mathrm{Al}}/^{27}\mathrm{Al}=5.23\times10^{-5}$, and $\lambda=\ln2/t_{1/2}$ is the decay constant.

\begin{align}
  \delta^nX=\left[\frac{\left(\frac{^nX}{^mX}_\mathrm{sample}\right)}{\left(\frac{^nX}{^mX}_\mathrm{standard}\right)}-1\right]\times1000
\end{align}

```python
import matplotlib.pyplot as plt
import numpy as np
from scipy.optimize import curve_fit

fig, ax = plt.subplots(dpi=300)
func = lambda x, m, b : m*x+b # linear model function
markers = ['o', 'o', 'o', 'o', 'D', 'D', 'D', 'D']
xfits = np.asarray([0, 7])
hl = 0.73                     # 26Al half-life / Ma
lambd = np.log(2)/hl          # 26Al decay constant
canon = 5.23e-5               # canonical 26Al/27Al

popt, pcov = curve_fit(func, Al_Mgs, deltaMgs, sigma=sigma2s/2)
perr = np.sqrt(np.diag(pcov))
slope3 = 5.8e-6
dt_cai = np.abs(np.log(popt[0]/canon)/lambd)
dt_cai3 = np.abs(np.log(slope3/canon)/lambd)
# dt_cai = 1/lambd - np.log(canon/popt[0]) # Brandon Scott's formula
t_caierr = 0                  # TO DO
mswd = 0                      # TO DO
lhs1 = r'$(^{26}\mathrm{Al}/^{27}\mathrm{Al})_0=$'
lhs2 = r'$(\mathrm{\delta}^{26}\mathrm{Mg}^*)_0=$'
lhs3 = r'$\Delta t_\mathrm{CAI}=$'
ln1 = r'$\mathbf{NWA\,8276\,Ch9}$'+'\n'
ln2 = lhs1 + f'{popt[0]:.1e}$\pm${perr[0]:.1e}\n'
ln3 = lhs2 + f'{popt[1]:.3f}$\pm${perr[1]:.3f} ‰\n'
ln4 = lhs3 + f'{dt_cai:.2f} Ma\n' #$\pm${t_caierr:.2f}
ln5 = r'$\mathbf{From\,Fig. \,3}$'+'\n'
ln6 = lhs1 + f'{slope3:.2e}\n'
ln7 = lhs3 + f'{dt_cai3:.2f} Ma'
# ln5 = f'MSWD {mswd:.2f}'
label = ln1+ln2+ln3+ln4+ln5+ln6+ln7
ax.errorbar(Al_Mgs[:4], deltaMgs[:4], sigma2s[:4], c='black', ls='',
            marker='o', mfc='tab:blue', mew=0, )
ax.errorbar(Al_Mgs[4:], deltaMgs[4:], sigma2s[4:], c='black', ls='',
            marker='D', mfc='tab:green', mew=0, )
ax.plot(xfits, func(xfits, *popt), color='tab:red', label=label)
ax.grid()
ax.legend(loc='upper left')
ax.minorticks_on()
ax.set(xlabel=r'$^{27}\mathrm{Al}/^{24}\mathrm{Mg}$',
       ylabel=r'$\mathrm{\delta}^{26}\mathrm{Mg}* $(‰)',
       xlim=(0, 7), ylim=(-0.15, 0.45))
ax.tick_params(axis='both', which='both', top=True, right=True)
```


#  AI Workshop - Część I
## Wykorzystanie generatywnych autoenkoderów do ekstrakcji cech istotnych w procesie wyszukiwania podobnych twarzy

## Plan pracy:

####  >>> Zbudujemy autoenkoder wariacyjny zdolny do identyfikacji charakterystycznych cech twarzy.
####  >>> Wykorzystamy zbudowany autoenkoder do odnajdywania podobnych twarzy.
####  >>> Stworzymy nowe twarze :D

###  Do tego celu posłużymy się danymi pochodzącymi ze zbioru CelebA. Zbiór ten posiada ponad 200 tysięcy zdjęć twarzy znanych celebrytów.

### Uwaga: Miejsca w kodzie do uzupełnienia są oznaczone poprzez "____"

## Zaczniemy od zaimportowania niezbędnych bibliotek i ustawienia odpowiednich folderów
##### Wykorzystane funkcje zostaną wytłumaczone w dalszej części notatnika

```python
!pip install --user -r ../requirements.txt # pobieranie bibliotek z pliku requirements.txt
```

```python
# operacje związane ze ścieżkami
import os
from glob import glob

# operacje na macierzach
import numpy as np

# operacje na obrazach
from PIL import Image

# wizualizacja postępu wykonania kodu
from tqdm import tqdm

# tworzenie wykresów i wizualizacja danych
import matplotlib.pyplot as plt

# importy związane z budową i treningiem modelu
from sklearn.model_selection import train_test_split
from keras.layers import Dense, Input
from keras.layers import Conv2D, Flatten, Lambda
from keras.layers import Reshape, Conv2DTranspose
from keras.models import Model
from keras.datasets import mnist
from keras.losses import mse, kullback_leibler_divergence
from keras.utils import plot_model
from keras import backend as K
from keras.callbacks import EarlyStopping, ModelCheckpoint

# importy związane z wyszukiwaniem podobnych twarzy
from sklearn.metrics import pairwise_distances

%matplotlib inline
```

## Pobranie i rozpakowanie danych

*   Linux

```python
!wget https://www.dropbox.com/s/lpmjzzk26nae9bh/img_align_celeba.zip
!unzip -qq img_align_celeba.zip -d ..
```

*   Windows
Wejdź na stronę: https://www.dropbox.com/s/lpmjzzk26nae9bh/img_align_celeba.zip
Pobierz plik i wypakuj w utworzonym folderze (*img_align_celeba*)

```python
# tworzymy foldery na zdjęcia i modele
dataset_dir = '../img_align_celeba'
model_dir = '../models'

os.makedirs(dataset_dir, exist_ok=True)
os.makedirs(model_dir, exist_ok=True)
```

#### Zawsze warto ustawić tak zwane ziarno - określa ono punkt początkowy dla algorytmu generacji liczb pseudolosowych. Dzięki temu przy każdorazowym uruchomieniu notatnika uzyskamy te same wyniki.
Podpowiedź: ziarno ustawiamy podając dowolną liczbę do funkcji np.random.seed()

```python
# ustaw swoje ziarno
____
```

## Wczytanie i przygotowanie danych
#### Dane powinny znajdować się w folderze *dataset_dir*

```python
img_paths = glob(dataset_dir + "/*.jpg") # pobierze ścieżki do każdego z obrazów w folderze
```

#### Wszystkich obrazów powinno być 202599. Sprawdź, czy rzeczywiście tak jest i nic się nie zgubiło :)

```python
# wyświetl ilość obrazów wczytanych do notatnika
____
```

#### W celu przyspieszenia obliczeń wykorzystamy jedynie 10 000 losowo wybranych obrazów. Potrzebujemy zatem 10 000 losowo wybranych indeksów. Pozwala na to funkcja np.random.choice(), która przyjmuje dwa argumenty: pierwszy określa elementy z jakich losujemy (w naszym przypadku będzie to liczba wszystkich obrazów), a drugi to ilość liczb jakie zostaną zwrócone
##### *ustawione wcześniej ziarno sprawi, że zawsze wylosujemy ten sam zestaw liczb

```python
# Wygeneruj 10000 losowych indeksów
rnd_choice = _____
```

#### Razem z wczytaniem danych  (Image.open()) przygotujemy je do wykorzystania: przytniemy krawędzie (crop()) i zmniejszymy ich rozmiar (resize()), co znacznie przyspieszy trening modelu.
#### Zastosuj wylosowane indeksy w celu wczytania zbioru danych do pracy. Przytnij obraz wprowadzając wartości 9, 13, 169 i 205 (koordynaty pikseli) w odpowednie miejsce. Następnie zmniejsz obraz do rozmiaru 80x96

```python
images = np.array(
                [np.array(Image.open(img_paths[x]).crop(box=(____)).resize((____)))
                 for x in tqdm(rnd_choice.tolist())]
                ).astype(np.float32)
```

#### Żeby upewnić się, że wszystko poszło dobrze, wyświetl wymiary obrazów

```python
____
```

#### Dokończ skalowanie obrazów do wartości z przedziału [0,1], w każdym z kanałów RGB (3), wykorzystując funkcje biblioteki numpy (np.min(), np.max()) oraz poniższy wzór:
\begin{equation}
X = \frac{X - min(X)}{max(X) - min(X)}
\end{equation}

```python
for i in range(3):
    images[:,:,:,i] = (____ - ____)/(____ - ____)
```

#### Upewnij się, że z danymi wszystko w porządku zanim przejdziesz do ich analizy. Wyświetl przykładowe 9 obrazów

```python
plt.figure(figsize=(10,10))
for i in range(1,10):
    plt.subplot(3,3,i)
    plt.imshow(np.squeeze(images[i]))
    plt.axis('off')
```

## Budowa modelu wariacyjnego autoenkodera (VAE)

#### Na początku zdefiniujemy parametry określające model VAE i jego trening

```python
""" Parametry określające model """
kernel_size = 3 # zwyczajowa wielkość fitra konwolucyjnego
filters = 16 # liczba filtrów dla pierwszej warstwy konwolucyjnej, rośnie 2-krotnie dla następnych
layers = 3 # ilość warstw konwolucyjnych tworzących enkoder i dekoder
input_shape = images.shape[1:4] # wielkość obrazu wejściowego, wykorzystywana dla budowy grafu SN
n_dense = 256 # liczba neuronów dla środkowej warstwy ukrytej - perceptronu
latent_dim = 32 # liczba cech które uzyskamy z enkodera

""" Parametry określające trening sieci """
batch_size = 64 # liczba obrazów wykorzystywana do jednego kroku treningu SN
optimizer = 'adam' # funkcja odpowiadająca za redukcję błędu sieci
epochs = 30 # maksymalna liczba epok, czyli ile razy sieć będzie widziała dane treningowe
model_path = os.path.join(model_dir,'celeb_vae_v2.h5') # ustawienie ścieżki do pliku z modelem
```

#### Zdefiniujemy niezbędne funkcje: sampling() generującą nowe dane w warstwie ukrytej *z* oraz vae_loss() będącą funkcją straty wykorzystującą błąd MAE jak i dywergencję Kullbacka-Leiblera

```python
def sampling(args):
    """ Funkcja generująca wartości ze standardowych rozkładów normalnych """

    z_mean, z_log_var = args
    batch = K.shape(z_mean)[0]
    dim = K.int_shape(z_mean)[1]
    epsilon = K.random_normal(shape=(batch, dim))
    return z_mean + K.exp(0.5 * z_log_var) * epsilon
```

```python
def vae_loss():
    """ Funkcja strat (MSE + Kullback-Leibler) """

    reconstruction_loss = mse(K.flatten(inputs), K.flatten(outputs))
    reconstruction_loss *= input_shape[0] * input_shape[1]
    kl_loss = 1 + z_log_var - K.square(z_mean) - K.exp(z_log_var)
    kl_loss = K.sum(kl_loss, axis=-1)
    kl_loss *= -0.5
    vae_loss = K.mean(reconstruction_loss + kl_loss)

    return vae_loss
```

### Na podstawie poniższego schematu i wskazówek zbuduj model wariacyjnego autoenkodera (uzupełnij brakujące części kodu)

###  Ważne wskazówki :
*   Zwróć uwagę na kolory warstw na powyższym schemacie i nazwy w nawiasach
*   Brakujące warstwy uzupełniaj analogicznie do wpisanych już przykładów
*   Uzupełnij brakujące pola "____" w wyznaczonych miejscach
*   Funkcja aktywacji w warstwach konwolucyjnych to 'relu'
*   Dla każdej kolejnej warstwy konwolucyjnej (W ENKODERZE!) liczbę filtrów zwiększ dwukrotnie w stosunku do poprzedniej
*   Dla każdej kolejnej warstwy konwolucyjnej transponowanej (W DEKODERZE!) liczbę filtrów zmniejsz dwukrotnie w stosunku do poprzedniej
*   Stosujemy padding z zachowaniem wymiarów ('same')

```python
# ENKODER

inputs = Input(shape=input_shape, name='encoder_input') # stworzenie wejścia sieci neuronowej
x = inputs

x = Conv2D(filters=filters*2, kernel_size=kernel_size,
           activation='relu', strides=2, padding='same')(x) #pierwsza warstwa konwolucyjna enkodera

# TU DODAJ BRAKUJĄCE WARSTWY: x = ___ itp, ze zwiększoną DWUKROTNIE liczbą filtrów w stosunku do poprzedniej warstwy (*4, *8)
x = ____(____)(x)
x = ____(____)(x)

shape = K.int_shape(x) # zapisanie wielkości wyjścia warstwy do późniejszego stworzenia dekodera

x = ____()(x) #SPŁASZCZENIE danych do formy wektora
x = Dense(n_dense, activation=____)(x) #dodatkowa pośrednia warstwa pełna

# trenowalne średnie oraz wariancje do celu generacji wektora ukrytego
z_mean = ____(latent_dim, name='z_mean')(x)
z_log_var = _____(latent_dim, name='z_log_var')(x)

z = ____(sampling, output_shape=(latent_dim,), name='z')([z_mean, z_log_var]) # wartości wyjściowe z enkodera

encoder = Model(inputs, [z_mean, z_log_var, z], name='encoder') # stworzenie enkodera

# DEKODER

latent_inputs = Input(shape=(latent_dim,), name='z_sampling') # stworzenie wejścia dla dekodera

x = ____(n_dense, activation=____)(latent_inputs) # pośrednia warstwa pełna

x = Dense(shape[1] * shape[2] * shape[3], activation=____)(x)
x = Reshape((shape[1], shape[2], shape[3]))(x) # wektor -> macierz

x = Conv2DTranspose(filters=filters*4, kernel_size=____,
                    activation=____, strides=2, padding=____)(x) #pierwsza warstwa konwolucyjna dekodera

# TU DODAJ BRAKUJĄCE WARSTWY: x = ___ itp, ze zmniejszoną DWUKROTNIE liczbą filtrów w stosunku do poprzedniej warstwy (*2, *1)
x = ____(____)(x)
x = ____(____)(x)

outputs = _____(filters=3, kernel_size=kernel_size, activation='sigmoid',
                padding='same', name='decoder_output')(x) # warstwa wyjściowa

decoder = Model(____, ____, name='decoder') # stworzenie dekodera (wpisz dane wejściowe i wyjściowe z dekodera)

# stworzenie modelu VAE

outputs = decoder(encoder(inputs)[2])
vae = Model(inputs, outputs, name='vae')
```

## Trening modelu wariacyjnego autoenkodera (VAE)

### Przygotowanie do treningu
#### Podział danych na treningowe i testowe

```python
X_train, X_test = train_test_split(images, test_size = 0.2)
```

#### Wprowadzenie parametrów treningowych i funkcji monitorujących przebieg treningu

```python
vae.add_loss(vae_loss())
vae.compile(optimizer=optimizer)

early_stop = EarlyStopping(restore_best_weights=True, patience=3) # zatrzyma uczenie jeśli nie będzie poprawy przez 3 epoki
checkpt = ModelCheckpoint(model_path, save_best_only=True, save_weights_only=True) # zapisze wagi dla najlepszej epoki
```

### Trening

```python
history = vae.fit(X_train,
                  epochs=epochs,
                  batch_size=batch_size,
                  validation_data=(X_test, None),
                  verbose=2,
                  callbacks = [early_stop, checkpt])
```

## Weryfikacja rezultatów

#### Wczytanie zapisanych wag z epoki, dla której wartość funkcji straty była najniższa

```python
vae.load_weights(model_path)
```

#### Jeśli zastosujemy .summary() na utworzonym modelu, uzyskamy informacje o jego budowie. Wyświetl budowę enkodera oraz dekodera oraz odpowiedz na pytania

```python
print(____.summary()) # budowa enkodera

print(____) # budowa dekodera
```

#### Jak zmienił się kształt obrazów wejściowych po pierwszej warstwie konwolucyjnej enkodera? : _____
#### Jaka jest całkowita liczba trenowalnych parametrów w dekoderze? : _____
#### Jakie są wymiary filtrów wychodzących z drugiej warstwy konwolucyjnej transponowanej dekodera? : _____
#### Dlaczego w podanych wymiarach na każdej z warstw na pierwszej pozycji pojawia się None? : _____

### Dokonaj predykcji danych testowych na enkoderze i dekoderze (wyjściowa macierz z z enkodera jest wejściem do dekodera):

```python
z_mean, z_std_var, z = encoder.predict(X_test)

y_pred = decoder.____(____)
```

#### Wyświetl wymiary wyjściowej macierzy z. Czym są wiersze a czym kolumny tej macierzy ?

```python
# wymiary macierzy z:
____
```

### Wyświetlenie zdjęć oryginalnych i wyjściowych z autoenkodera

```python
fig, ax = plt.subplots(10,2, figsize = (10,45))
for i in range(10):
    ax[i,0].imshow(np.squeeze(X_test[i]))
    ax[i,0].set_title('Prawdziwe zdjęcie')
    ax[i,0].axis('off')
    ax[i,1].imshow(np.squeeze(y_pred[i]))
    ax[i,1].set_title('Wyjście z VAE')
    ax[i,1].axis('off')
plt.show()
```

## Wyszukiwanie twarzy najbardziej do siebie podobnych

### Do wybranego przez siebie zdjęcia twarzy odnajdź 5 najbardziej podobnych testowych przykładów.
#### Wybierz zdjęcie i wyświetl je:

```python
# wybierz jeden obraz z danych testowych (X_test) poprzez indeksowanie

index = ____ # wybierz numer przykładu
ref = ____ # wyciągnij obraz o indeksie 'index' z danych testowych

plt.imshow(np.squeeze(ref))
plt.axis('off')
plt.show()
```

#### Dla wybranego obrazu wyświetl jego wersję wychodzącą z dekodera:

```python
y_pred_ref = y_pred[index]

plt.imshow(np.squeeze(y_pred_ref))
plt.axis('off')
plt.show()
```

### Wylicz odległość pomiędzy wybranym obrazem a pozostałymi przykładami testowymi. Wyliczony wektor odległości posortuj rosnąco i wybierz 5 najniższych indeksów - oznaczających najbardziej podobne przykłady do wybranego przez ciebie

#### Odległość jest wyliczana pomiędzy wyjściowymi wektorami cech z enkodera (odległość między wektorami z wybranego obrazu a pozostałych przykładów). Wykorzystamy odległość euklidesową (euclidean), ale możesz poeksperymentować z innymi metrykami (np. manhattan, correlation, sqeuclidean czy cosine)

```python
z_ref = z[index]
z_ref = z_ref.reshape(1, -1) # konwersja na macierz z jedną obserwacją (1 wiersz)

distances = pairwise_distances(____, _____, metric='____')

# distances to macierz zawierająca obliczone odległości pomiędzy wybranym obrazem (wektor cech) a każdym przykładem testowym
print(distances.shape)
```

#### Mając policzoną macierz odległości, posortuj ją rosnąco i wybierz 5 pierwszych indeksów. Wykorzystaj do tego celu funkcję z biblioteki numpy np.argsort()

```python
indices_sorted = _____(____)[0] # uzyskaj indeksy posortowanych wartości

closest = indices_sorted[:____] # wybierz 5 najbliższych
```

### Wyświetl odnalezione, najbardziej podobne obrazy
#### Najbliższe obrazy uzyskane z dekodera

```python
for i in closest:
    plt.imshow(np.squeeze(y_pred[i]))
    plt.axis('off')
    plt.show()
```

#### Najbliższe obrazy oryginalne

```python
for i in closest:
    plt.imshow(np.squeeze(X_test[i]))
    plt.axis('off')
    plt.show()
```

### Dyskusja:
### >>> Które obrazy są do siebie bardziej podobne (uzyskane z dekodera czy oryginalne wersje)?
### >>> Co jest przyczyną takiego rezultatu ?
### >>> Czy podobieństwa pomiędzy obrazami są wyraźnie widoczne ? Jeśli nie, jaka może być tego przyczyna ?

## Generowanie nowych twarzy

### VAE to model generatywny, co znaczy, że pozwala na stworzenie nowych, nieistniejących przykładów
#### Polega to na tym, że zamiast wprowadzać do dekodera wyjściowy wektor cech z enkodera, wprowadzamy wektory z losowo wybranymi liczbami próbkowanymi z rozkładu standardowego (średnia równa 0, odchylenie standardowe równe 1)
#### Możemy to osiągnąć za pomoca funkcji np.random.randn(), która zwraca losowe liczby z rozkładu N(0,1). Funkcja przyjmuje wartości określające rozmiar generowanej macierzy. W naszym przypadku musi być to wektor o długości wektora cech, wychodzącego z enkodera, dla jednego obrazu (jeden wiersz, ilość cech)

```python
fig, ax = plt.subplots(5,4, figsize = (10,10))
for i in range(5):
    for j in range(4):
        z_generated = np.random.randn(____,____) # stwórz wektor losowych liczb o długości wektora cech
        y_pred_generated = ____.predict(____) # uzyskaj predykcję z dekodera na wygenerowanym wektorze
        ax[i,j].imshow(np.squeeze(y_pred_generated))
        ax[i,j].axis('off')
plt.show()
```

### Czy wygenerowane twarze wyglądają na rzeczywiste? :)