(c) Juan Gomez 2019. Thanks to Universidad EAFIT for support. This material is part of the course Introduction to Finite Element Analysis

# SOLUTION: Application: Visualization in a Full Domain

## By Juan Gomez

### Class activity
Consider the theory of elasticity solution for a cantilever beam of height $h = 2c$, length $2\ell$ and moment of inertia $I$, subjected to a uniformly distributed load of intensity $q$ (Timoshenko and Goodier, 2011):

$$\sigma_{xx}=-\frac q{2I}\left(x^2y-\frac23y^3\right)$$

$$\sigma_{yy}=-\frac q{2I}\left(\frac13y^3-c^2y+\frac23c^3\right)$$

$$\tau_{xy}=-\frac q{2I}\left(c^2-y^2\right)x$$

* Identify the problem domain  and use the free three-dimensional mesh generator [Gmsh](http://gmsh.info/) to create a finite element mesh of this domain.

* Use the Python module [meshio](https://github.com/nschloe/meshio) to read the mesh from the **.msh** created file and convert it into the appropriate text files for nodes and elements.

* Use the interpolation and visualization subroutines discussed in the notebook to create an independent script (or notebook) and visualize the principal stresses over the computational domain.

We will use all the plotting subroutines defined in the original Notebook. The gmsh model is available in the files folder under the name beam.msh

```python
%matplotlib inline
import matplotlib.pyplot as plt
from matplotlib.tri import Triangulation, CubicTriInterpolator
import numpy as np
import sympy as sym
import meshio
import solidspy.preprocesor as msh
```

```python
def plot_SFIELD(UC, nodes, elements, Ngra, plt_type="contourf",  levels=12,
               savefigs=False, title="Solution:"  ):
    """Plots a user defined scalar field using a triangulation.
    Parameters
    ----------
    UC : ndarray (float)
      Array with the nodal solution.

    """
    tri = mesh2tri(nodes, elements)
    tri_plot(tri, UC , Ngra , title=r'$U_{var}$',
             figtitle=title + "User variable",
             levels=levels, plt_type=plt_type, savefigs=savefigs,
             filename="uservar.pdf")
```

```python
def mesh2tri(nodes, elements):
    """Generates a matplotlib.tri.Triangulation object from the mesh

    Parameters
    ----------
    nodes : ndarray (float)
      Array with number and nodes coordinates:
        `number coordX coordY BCX BCY`
    elements : ndarray (int)
      Array with the node number for the nodes that correspond to each
      element.

    Returns
    -------
    tri : Triangulation
        An unstructured triangular grid consisting of npoints points
        and ntri triangles.

    """
    x = nodes[:, 1]
    y = nodes[:, 2]
    triangs = []
    for el in elements:
        if el[1]==1:
            triangs.append(el[[3, 4, 5]])
            triangs.append(el[[5, 6, 3]])
        if el[1]==2:
            triangs.append(el[[3, 6, 8]])
            triangs.append(el[[6, 7, 8]])
            triangs.append(el[[6, 4, 7]])
            triangs.append(el[[7, 5, 8]])
        if el[1]==3:
            triangs.append(el[3:])

    tri = Triangulation(x, y, np.array(triangs))
#
    return tri
```

```python
def tri_plot(tri, field, Ngra ,  title="", figtitle="", levels=12, savefigs=False,
             plt_type="contourf" , filename="solution_plot.pdf"  ):

    plt.figure(Ngra)
    if plt_type=="pcolor":
        disp_plot = plt.tripcolor
    elif plt_type=="contourf":
        disp_plot = plt.tricontourf
    plt.figure(figtitle)
    disp_plot(tri, field, levels)
    plt.title(title)
    plt.colorbar(orientation='vertical')
    plt.axis("image")
    plt.grid()
```

#### Mesh reading and stress function.

The following script uses the functions **node_writer()** and **ele_writer()** defined in the preprocessing module from SolidsPy to convert the gmsh file (beam.msh) into nodal and element input files.

```python
def script_mesh(mesh):

    points = mesh.points
    cells  = mesh.cells
    point_data = mesh.point_data
    cell_data  = mesh.cell_data
    field_data = mesh.field_data

    nodes_array    = msh.node_writer(points, point_data)
    nf, els_array  = msh.ele_writer(cells, cell_data, "triangle", 100, 3, 0, 0)

    np.savetxt("files/Beles.txt", els_array, fmt="%d")
    np.savetxt("files/Bnodes.txt", nodes_array, fmt=("%d", "%.4f", "%.4f", "%d", "%d"))

    return
```

```python
def principal_stress_field(x, y, q , c):
    I = 2/3*c**3
    # Stress field
    S_xx = -(q/2/I)*(y*x**2-2/3*y**3.)
    S_yy = -(q/2/I)*(1/3*y**3-y*c**2+2/3*c**3)
    T_xy =  -(q/2/I)*(1/3*y**3-y*c**2+2/3*c**3)*x
    #Principal stresses
    sig_c = (S_xx+S_yy)/2
    Rsq = ((S_xx-S_yy)/2)**2 + T_xy**2
    R = np.sqrt(Rsq)
    sig_p = sig_c + R
    sig_m = sig_c - R
    return sig_p , sig_m , R
```

#### Main code

* Reads the model.

* Extracts nodal coordinates

* Evaluates the solution and plots.ion of the solution array **SOL[]**.

```python
mesh = meshio.read("files/beam.msh")
script_mesh(mesh)
nodes        = np.loadtxt('files/'+'Bnodes.txt')
elements     = np.loadtxt('files/'+'Beles.txt')
nn =len(nodes[:,0])
```

```python
coords=np.zeros([nn,2])
coords[:,0]=nodes[:,1]
coords[:,1]=nodes[:,2]
SOL_p = np.zeros([nn])
SOL_m = np.zeros([nn])
SOL_r = np.zeros([nn])
```

```python
q = 1.0
c = 1.0
for i in range(0,nn):
    x = coords[i,0]
    y = coords[i,1]
    Sig_p , Sig_m , r = principal_stress_field(x, y, q , c)
    SOL_p[i] = Sig_p
    SOL_m[i] = Sig_m
    SOL_r[i] = r
plot_SFIELD(SOL_p , nodes , elements, 0 , plt_type ="contourf", levels = 12 )
```

```python
plot_SFIELD(SOL_m , nodes , elements, 1 , plt_type ="contourf", levels = 12 )
```

### References

* Juan Gómez, Nicolás Guarín-Zapata (2018). SolidsPy: 2D-Finite Element Analysis with Python, <https://github.com/AppliedMechanics-EAFIT/SolidsPy>.

* Timoshenko, S.P., and Goodier, J.N. (1976). Theory of Elasticity. International Student Edition. McGraw-Hill International.

```python
plot_SFIELD(SOL_r , nodes , elements, 2 , plt_type ="contourf", levels = 12 )
```

```python
from IPython.core.display import HTML
def css_styling():
    styles = open('./nb_style.css', 'r').read()
    return HTML(styles)
css_styling()
```

<link href='http://fonts.googleapis.com/css?family=Fenix' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Alegreya+Sans:100,300,400,500,700,800,900,100italic,300italic,400italic,500italic,700italic,800italic,900italic' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Source+Code+Pro:300,400' rel='stylesheet' type='text/css'>

```python

```


```python
import sympy as sym
import matplotlib.pyplot as plt
%matplotlib inline
sym.init_printing()
```

# Caracterización de Sistemas en el Dominio del Tiempo

*This Jupyter notebook is part of a [collection of notebooks](../index.ipynb) in the bachelors module Signals and Systems, Communications Engineering, Universität Rostock. Please direct questions and suggestions to [Sascha.Spors@uni-rostock.de](mailto:Sascha.Spors@uni-rostock.de).*

## Convolución

A partir del estudio de la respuesta impulsional se observa que la [convolución](https://en.wikipedia.org/wiki/Convolution) resulta una operación relevante en el análisis de sistemas.
En esta sección, se estudiarán las propiedades y se interpretará la convolución.

La  convolución de dos señales $x(t)$ y $h(t)$ es:

\begin{equation}
(x * h)(t) = x(t) * h(t) = \int_{-\infty}^{\infty} x(\tau) \cdot h(t - \tau) \; d\tau =
\int_{-\infty}^{\infty} x(t - \tau) \cdot h(\tau) \; d\tau
\end{equation}

Note que $*$ se usa como notación para la convolución.

### Propiedades

Dadas las señales $x(t)$, $h(t)$, $g(t) \in \mathbb{C}$ la convolución cumple con:

\begin{equation}
(x * h)(t) = x(t) * h(t) = \int_{-\infty}^{\infty} x(\tau) \cdot h(t - \tau) \; d\tau =
\int_{-\infty}^{\infty} x(t - \tau) \cdot h(\tau) \; d\tau
\end{equation}

1. El impulso de Dirac es el [elemento identidad](https://en.wikipedia.org/wiki/Identity_element) de la convolución
    \begin{equation}
    x(t) * \delta(t) = x(t)
    \end{equation}

2. La convolución es [conmutativa](https://en.wikipedia.org/wiki/Commutative_property)
    \begin{equation}
    x(t) * h(t) = h(t) * x(t)
    \end{equation}

3. La convolución es [asociativa](https://en.wikipedia.org/wiki/Associative_property)
    \begin{equation}
    \left[ x(t) * h(t) \right] * g(t) = x(t) * \left[ h(t) * g(t) \right]
    \end{equation}

5. La convolución es [distributiva](https://en.wikipedia.org/wiki/Distributive_property)
    \begin{equation}
    x(t) * \left[ h(t) + g(t) \right] = x(t) * h(t) + x(t) * g(t)
    \end{equation}

5. Multiplicación por un escalar $a \in \mathbb{C}$
    \begin{equation}
    a \cdot \left[ x(t) * h(t) \right] = \left[ a \cdot x(t) \right] * h(t) = x(t) * \left[ a \cdot h(t) \right]
    \end{equation}

6. Derivada de la convolución
    \begin{equation}
    \frac{d}{dt} \left[ x(t) * h(t) \right] =  \frac{d x(t)}{dt} * h(t) = x(t) * \frac{d h(t)}{dt}
    \end{equation}

### Interpretación

La convolución puede someterse a una [interpretación gráfica](https://en.wikipedia.org/wiki/Convolution#Visual_explanation).

La operación de convolución:

\begin{equation}
y(t) = \int_{-\infty}^{\infty} x(\tau) \cdot h(t-\tau) \; d\tau
\end{equation}

Puede descomponerse en:

1. sustituir $t$ por $\tau$ en $x(t)$ y $h(t)$,

2. invertir el tiempo en $h(\tau)$,

3. desplazar $h(-\tau)$ un tiempo $t$ para obtener $h(t - \tau)$,

4. buscar los traslapes temporales entre $h(t - \tau)$ y $x(\tau)$, calcular la integral en el periodo de traslape para obtener $y(t)$

Considere el siguiente ejemplo.

**Ejemplo**

La convolución $y(t) = x(t) * h(t)$ se ilustra para un par de señales definidas.

\begin{align}
h(t) &= e^{-t} \\
x(t) &= \frac{3}{4} \cdot \text{rect} \left(t - \frac{1}{2}\right)
\end{align}

(Se van a definir nuevas funciones para facilitar los códigos.)

```python
t, tau = sym.symbols('t tau', real=True)

class rect(sym.Function):

    @classmethod
    def eval(cls, arg):
        return sym.Heaviside(arg + sym.S.Half) - sym.Heaviside(arg - sym.S.Half)

def plot_signals(x_axis, x, h, ylabel, xlabel):
    plt.rcParams['figure.figsize'] = 7, 2

    p1 = sym.plot(x, (x_axis, -5, 5), show=False,
                  line_color='C0', ylabel=ylabel, xlabel=xlabel,legend=True, label = '$x$')
    p2 = sym.plot(h, (x_axis, -5, 5), show=False, line_color='C1',legend=True, label = '$h$')
    p1.extend(p2)
    p1.show()
```

Se definen y grafican las señales.

```python
h = sym.exp(-t) * sym.Heaviside(t)
x = sym.Rational(3, 4) * rect(t - 1/2)

plot_signals(t, x, h, r'$h(t)$, $x(t)$', r'$t$')
```

**Primero** se sustituye $t$ por $\tau$ para obtener $h(\tau)$ y $x(\tau)$.
El eje horizontal representará a $\tau$, la variable de integración.

```python
h1 = h.subs(t, tau)
x1 = x.subs(t, tau)

plot_signals(tau, x1, h1, r'$h(\tau)$, $x(\tau)$', r'$\tau$')
```

**Segundo** se invierte el tiempo de $h(\tau)$ para obtener $h(-\tau)$.

```python
h2 = h.subs(t, -tau)

plot_signals(tau, x1, h2, r'$h(-\tau)$, $x(\tau)$', r'$\tau$')
```

**Tercero** la señal $h(-\tau)$ se desplaza un tiempo $t$ para obtener $h(t - \tau)$. Se desplaza a la **derecha** si $t>0$ y a la **izquierda** si $t<0$.

```python
h3 = h.subs(t, t-tau)
h3
```

**Cuarto** se buscan los traslapes temporales entre $h(t - \tau)$ y $x(\tau)$ para calcular la integral en el periodo de traslape para obtener $y(t)$

La señal $x(\tau)$ se "enciende" en $t=0$ y se "apaga" en $t=1$. Así, se pueden identificar 3 casos para las señales definidas.

1. si $t<0$ no hay traslape,
2. si $0 \leq t < 1$ hay traslape parcial,
3. si $t > 1$ hay traslape completo.

1. si $t<0$ no hay traslape

```python
plot_signals(tau, x1, h3.subs(t, -2), r'$h(t-\tau)$, $x(\tau)$', r'$\tau$')
```

### Solución A
Primero, se va a resolver la ODE teniendo en cuenta  que deben satisfacerse las condiciones iniciales $y(t)\big\vert_{t = 0-} = 0$ y $\frac{d}{dt}y(t)\big\vert_{t = 0-} = 0$ relacionadas con la causalidad.

a. Defina la ODE en`SymPy`

```python

```

```python
t = sym.symbols('t', real=True)
x = sym.Function('x')(t)
y = sym.Function('y')(t)

ode = sym.Eq(y + y.diff(t), x)
ode
```

Se resuelve la ODE para la entrada establecida y considerando que las constantes de integración deben garantiza las condiciones inicales.

```python
xinput = sym.exp(-2*t)*sym.Heaviside(t)

solution = sym.dsolve(ode.subs(x, xinput))
solution
```

```python

integration_constants = sym.solve(
    (solution.rhs.limit(t, 0, '-'), solution.rhs.diff(t).limit(t, 0, '-')), 'C1')

y1 = solution.subs(integration_constants)
y1
```

Se grafica la señal de salida obtenida al resolver la ODE.

```python
plt.rcParams['figure.figsize'] = 7, 2
sym.plot(y1.rhs, (t,-1,10), ylabel=r'$y(t)$');
```

### Solución B
Ahora se va a resolver el problema a través de la respuesta impulsional.

La respuesta impulsional $h(t)$ se encuentra al resolver la ODE para una señal impulso de Dirac como entrada, $x(t) = \delta(t)$.

```python
h = sym.Function('h')(t)

solution2 = sym.dsolve(ode.subs(x, sym.DiracDelta(t)).subs(y, h))

integration_constants = sym.solve((solution2.rhs.limit(
    t, 0, '-'), solution2.rhs.diff(t).limit(t, 0, '-')), 'C1')

h = solution2.subs(integration_constants)
h
```

La respuesta impulsional se grafica.

```python
plt.rcParams['figure.figsize'] = 7, 2
sym.plot(h.rhs, (t,-1,10), ylabel=r'$h(t)$');
```

Ahora se calcula la convolución $y(t) = x(t) * h(t)$ como:

\begin{equation}
y(t) = \int_{-\infty}^{\infty}  x(\tau) \cdot h(t-\tau) \; d \tau
\end{equation}

Debido a que $h(t)$ y $x(t)$ son señales causales, la expresión de convolución se convierte en:

\begin{equation}
y(t) = \int_{0}^{t} x(\tau) \cdot h(t - \tau) \; d\tau \; \forall t \geq 0
\end{equation}

Observe que $y(t) = 0 \; \forall t<0$.

```python
tau = sym.symbols('tau', real=True)

y2 = sym.integrate(xinput.subs(t, tau) * h.rhs.subs(t, t-tau), (tau, 0, t))
y2
```

Se grafica la señal de salida obtenida mediante la convolución.

```python
plt.rcParams['figure.figsize'] = 7, 2
sym.plot(y2, (t,-1,10), ylabel=r'$y(t)$');
```

Las señales obtenidas con los dos métodos deben ser iguales.  Verifiquemos las dos señales.

```python
y1
```

```python
y2
```

Al comparar las dos señales graficamente se obtiene:

```python
plt.rcParams['figure.figsize'] = 7, 2
graf1 = sym.plot(y1.rhs, (t,-1,10), ylabel=r'$y(t)$', show=False, line_color='red',legend=True, label = 'ODE')
graf2 = sym.plot(y2, (t,-1,10), show=False, legend=True, label = 'convolucion')
graf1.extend(graf2)
graf1.show()
```


```python
import torch
import torch.nn as nn
import numpy as np
from numpy.random import default_rng

def set_seed(seed=0):
    seed = 42069  # set a random seed for reproducibility
    torch.backends.cudnn.deterministic = True
    torch.backends.cudnn.benchmark = False
    np.random.seed(seed)
    torch.manual_seed(seed)
    if torch.cuda.is_available():
        torch.cuda.manual_seed_all(seed)

```

```python

def init_weight():
    # y0 dim: (1, 2)
    w1 = np.array([[0.2, 0.3],
                   [0.4, 0.2],
                   [0.3, 0.4]], dtype='f')
    # y1 dim: (1, 3)
    w2 = np.array([[0.2, 0.3, 0.4],
                   [0.4, 0.2, 0.3],
                   [0.3, 0.4, 0.2]], dtype='f')
    # y2 dim: (1, 3)
    w3 = np.array([[0.2, 0.3, 0.4],
                   [0.4, 0.2, 0.3]], dtype='f')
    # y3 dim: (1, 2)
    return w1, w2, w3

class CMlp(nn.Module):

    def __init__(self, encrypt=False):
        super(CMlp, self).__init__()
        w1, w2, w3 = init_weight()

        self.fc1 = nn.Linear(2, 3, False)
        self.fc1.weight.data = torch.from_numpy(w1)
        self.relu1 = nn.ReLU()
        self.fc2 = nn.Linear(3, 3, False)
        self.fc2.weight.data = torch.from_numpy(w2)
        self.relu2 = nn.ReLU()
        self.fc3 = nn.Linear(3, 2, False)
        self.fc3.weight.data = torch.from_numpy(w3)
        if encrypt:
            rng = default_rng(0)
            self.r1 = np.absolute(rng.standard_normal((3, 1), dtype='f'))
            self.r2 = np.absolute(rng.standard_normal((3, 1), dtype='f'))
            self.r3 = np.absolute(rng.standard_normal((2, 1), dtype='f'))
            self.fc1.weight.data = torch.from_numpy(w1 * self.r1)
            self.fc2.weight.data = torch.from_numpy(w2 * self.r2 / self.r1.transpose())
            self.fc3.weight.data = torch.from_numpy(w3 / self.r2.transpose() + self.r3)
        self.y2 = None
        self.y3 = None
        self.alpha = None
    def forward(self, x):
        y1 = self.fc1(x)
        self.y2 = self.fc2(self.relu1(y1))
        self.alpha = self.y2.sum()
        self.y3 = self.fc3(self.relu2(self.y2))
        self.y3.retain_grad()
        return self.y3
```

```python
# setup gpu or cpu
# device = 'cuda' if torch.cuda.is_available() else 'cpu'
device = 'cpu'

r = (0.2, 0.4, 0.8)
x = torch.tensor([[0.2, 0.3]], device=device)
y_hat = torch.tensor([[0.5, 0.5]], device=device)

net = CMlp().to(device)
print('----------- plaintext weight ---------------')
for p in net.parameters():
    print(p.data)
y = net(x)
print('y: ', y)
criterion = nn.MSELoss()
loss = criterion(y, y_hat)
loss.backward(retain_graph=True)
print('----------- plaintext grad -----------------')
for p in net.parameters():
    print(p.grad)
w_gradlist = [p.grad for p in net.parameters()]
print('----------- ciphertext weight ---------------')
net_c = CMlp(encrypt=True).to(device)
for p in net_c.parameters():
    print(p.data)
y_c = net_c(x)
c_loss = criterion(y_c, y_hat)
c_loss.backward(retain_graph=True)
print('----------- ciphertext grad ---------------')
for p in net_c.parameters():
    print(p.grad)
c_w_gradlist = [p.grad for p in net_c.parameters()]

r_a = torch.from_numpy(net_c.r3).to(device)
print('Get yc: ', y_c)
print('Get yc from y: ', y + net_c.alpha * r_a.t())
print('Ly derivative')
print(y - y_hat)
print(y.grad)
print('Lhaty derivative')
print(y_c - y_hat)
print(y_c.grad)
y_c_grad = y_c.grad
```

    ----------- plaintext weight ---------------
    tensor([[0.2000, 0.3000],
            [0.4000, 0.2000],
            [0.3000, 0.4000]])
    tensor([[0.2000, 0.3000, 0.4000],
            [0.4000, 0.2000, 0.3000],
            [0.3000, 0.4000, 0.2000]])
    tensor([[0.2000, 0.3000, 0.4000],
            [0.4000, 0.2000, 0.3000]])
    y:  tensor([[0.1206, 0.1221]], grad_fn=<MmBackward>)
    ----------- plaintext grad -----------------
    tensor([[-0.0401, -0.0602],
            [-0.0424, -0.0636],
            [-0.0401, -0.0602]])
    tensor([[-0.0295, -0.0318, -0.0409],
            [-0.0246, -0.0265, -0.0341],
            [-0.0345, -0.0371, -0.0477]])
    tensor([[-0.0531, -0.0508, -0.0497],
            [-0.0529, -0.0506, -0.0495]])
    ----------- ciphertext weight ---------------
    tensor([[0.2235, 0.3353],
            [0.5548, 0.2774],
            [0.1280, 0.1706]])
    tensor([[0.1438, 0.1738, 0.7535],
            [0.2153, 0.0867, 0.4230],
            [0.0201, 0.0216, 0.0352]])
    tensor([[0.3086, 0.5585, 5.3935],
            [0.5298, 0.3645, 4.0324]])
    ----------- ciphertext grad ---------------
    tensor([[-0.0376, -0.0564],
            [-0.0317, -0.0476],
            [-0.1000, -0.1500]])
    tensor([[-0.0451, -0.0602, -0.0238],
            [-0.0495, -0.0661, -0.0261],
            [-0.5054, -0.6755, -0.2671]])
    tensor([[-0.0413, -0.0296, -0.0036],
            [-0.0418, -0.0299, -0.0036]])
    Get yc:  tensor([[0.1327, 0.1286]], grad_fn=<MmBackward>)
    Get yc from y:  tensor([[0.1327, 0.1286]], grad_fn=<AddBackward0>)
    Ly derivative
    tensor([[-0.3794, -0.3779]], grad_fn=<SubBackward0>)
    tensor([[-0.3794, -0.3779]])
    Lhaty derivative
    tensor([[-0.3673, -0.3714]], grad_fn=<SubBackward0>)
    tensor([[-0.3673, -0.3714]])

$ \frac{\partial \widehat{L}}{\partial \widehat{W}^{(l)}}  $

```python
optim = torch.optim.Optimizer(net.parameters(), {})
optim_c = torch.optim.Optimizer(net_c.parameters(), {})
```

### set grad to zero

```python
for p in net_c.parameters():
    print(p.grad)
optim_c.zero_grad()
for p in net_c.parameters():
    print(p.grad)
```

    tensor([[-0.0376, -0.0564],
            [-0.0317, -0.0476],
            [-0.1000, -0.1500]])
    tensor([[-0.0451, -0.0602, -0.0238],
            [-0.0495, -0.0661, -0.0261],
            [-0.5054, -0.6755, -0.2671]])
    tensor([[-0.0413, -0.0296, -0.0036],
            [-0.0418, -0.0299, -0.0036]])
    tensor([[0., 0.],
            [0., 0.],
            [0., 0.]])
    tensor([[0., 0., 0.],
            [0., 0., 0.],
            [0., 0., 0.]])
    tensor([[0., 0., 0.],
            [0., 0., 0.]])

## get $\frac{\partial \alpha}{\partial \widehat{w}^{(l)}}$

```python
net_c.alpha.backward(retain_graph=True)
alpha_gradlist = [p.grad.detach().clone() for p in net_c.parameters()]
for p in net_c.parameters():
    print(p.grad)
```

    tensor([[0.0758, 0.1138],
            [0.0564, 0.0846],
            [0.2423, 0.3635]])
    tensor([[0.1453, 0.1942, 0.0768],
            [0.1453, 0.1942, 0.0768],
            [0.1453, 0.1942, 0.0768]])
    tensor([[0., 0., 0.],
            [0., 0., 0.]])

### set grad to zero and get $\mathbf{r}^t \frac{\partial \widehat{y}^{L}}{\partial \widehat{w}^{(l)}}$

```python
optim_c.zero_grad()
r = r_a.t()
for p in net_c.parameters():
    print(p.grad)
y_c.backward(r, retain_graph=True)
```

    tensor([[0., 0.],
            [0., 0.],
            [0., 0.]])
    tensor([[0., 0., 0.],
            [0., 0., 0.],
            [0., 0., 0.]])
    tensor([[0., 0., 0.],
            [0., 0., 0.]])

```python
c_yw_gradlist = [p.grad for p in net_c.parameters()]
```

```python
print(c_w_gradlist[2])
```

    tensor([[0.0067, 0.0048, 0.0006],
            [0.0036, 0.0026, 0.0003]])

```python
print(w_gradlist[2])
print(c_yw_gradlist[2])
```

    tensor([[-0.0531, -0.0508, -0.0497],
            [-0.0529, -0.0506, -0.0495]])
    tensor([[0.0067, 0.0048, 0.0006],
            [0.0036, 0.0026, 0.0003]])

Compute $\frac{\partial \widehat{L}}{\partial \widehat{W}}$

\begin{equation}
\frac{\partial \widehat{L}}{\partial \widehat{W}} = \frac{1}{R^{(l)}} \circ \frac{\partial L}{\partial W} + r^T \cdot \alpha \frac{\partial \widehat{y}^{(L)}}{\partial \widehat{W}} + r^T \cdot (\frac{ \partial \widehat{L}}{\partial \widehat{y}^{(L)}})^{T} \frac{\partial \alpha}{\partial \widehat{W}} - r^T r \alpha \frac{\partial \alpha}{\partial \widehat{W}}
\end{equation}

## Layer3 c_w3 grad with simple computing

\begin{equation}
\frac{\partial \widehat{L}}{\partial \widehat{W}} = \frac{1}{R^{(l)}} \circ \frac{\partial L}{\partial W} + r^T \cdot \alpha \frac{\partial \widehat{y}^{(L)}}{\partial \widehat{W}}
\end{equation}

```python
print(w_gradlist[2] * net_c.r2.transpose() + c_yw_gradlist[2] * net_c.alpha)
```

    tensor([[-0.0413, -0.0296, -0.0036],
            [-0.0418, -0.0299, -0.0036]], grad_fn=<AddBackward0>)

```python
print(y_c_grad.reshape(1, -1))
t = r.matmul(y_c_grad.reshape(1, -1).t())
```

    tensor([[-0.3673, -0.3714]])

```python
print(w_gradlist[1])
print(alpha_gradlist[1])
```

    tensor([[-0.0295, -0.0318, -0.0409],
            [-0.0246, -0.0265, -0.0341],
            [-0.0345, -0.0371, -0.0477]])
    tensor([[0.1453, 0.1942, 0.0768],
            [0.1453, 0.1942, 0.0768],
            [0.1453, 0.1942, 0.0768]])

## Layer2 c_w2 grad

```python
print(w_gradlist[1] / net_c.r2 * net_c.r1.transpose() + c_yw_gradlist[1] * net_c.alpha + alpha_gradlist[1] * t - r.matmul(r.t()) * net_c.alpha * alpha_gradlist[1])
```

    tensor([[-0.0451, -0.0602, -0.0238],
            [-0.0495, -0.0661, -0.0261],
            [-0.5054, -0.6755, -0.2671]], grad_fn=<SubBackward0>)

## Layer1 c_w1 grad

```python
print(w_gradlist[0] / net_c.r1 + c_yw_gradlist[0] * net_c.alpha + alpha_gradlist[0] * t - r.matmul(r.t()) * net_c.alpha * alpha_gradlist[0])
```

    tensor([[-0.0376, -0.0564],
            [-0.0317, -0.0476],
            [-0.1000, -0.1500]], grad_fn=<SubBackward0>)


# Лекция 3. Описание Линейных систем. Преобразование Лапласа

Здесь и далее мы будем работать с математическими моделями управляемых диамических объектов. Эти модели составляются на основе физических законов. На предыдущих занятиях мы обсуждали, что модель системы, как правило будет описываться системой обыкновенных дифференциальных уравнений, имеющей вид:

$$\dot{\mathbf{x}} = \mathbf{f}(\mathbf{x}, \mathbf{u}, t), \tag{1}$$

где $\mathbf{x}$ - вектор состояния системы, $\mathbf{u}$ - вектор управляющих параметров.

Довольно часто уравнение (1) представляет собой систему нелинейных дифференциальных уравнений, однако здесь речь пойдёт о тех системах, модель управляемой динамики которых может быть представлена (в силу постановки задачи или благодаря линеаризации уравнений (1)) системой линейных дифференциальных уравений.

Пространство состояний

Линейная непрерывная сстема управления описывается векторным линейным обыкновенным дифференциальным уравнением первого порядка:
$$\dot{\mathbf{x}} = \mathbf{A}\cdot \mathbf{x} + \mathbf{B} \cdot \mathbf{u} + \mathbf{D}_1 \cdot \mathbf{v}, \tag{2}$$

где $\mathbf{x} \in \mathbb{R}^n$ — вектор состояния,

$\mathbf{u} \in \mathbb{R}^m$ — вектор управляющих параметров,

$\mathbf{v} \in \mathbb{R}^p$ — вектор внешних возмущений или шумов, действующих на систему,

$\mathbf{A}$ — матрица состояния.

И управление $\mathbf{u}$, и возмущение $\mathbf{v}$ называют входами системы.

На практике часто бывает так, что состояние системы неизвестно (не измеряется непосредственно), а известен некоторый выход $\mathbf{y}(t) \in \mathbb{R}^l$:

$$ \mathbf{y} = \mathbf{C}\cdot \mathbf{x} + \mathbf{D}_2\cdot \mathbf{v}, \tag{3}$$

где $\mathbf{C}$ — матрица выхода.

Уравнение (2) принято называть уравнением состояния, уравнение (3) - уравнением выхода. Вместе они образуют описание системы в пространстве состояний.

Целью управления является такой выбор функции управления $\mathbf{u}(t)$ или $\mathbf{u}(\mathbf{x},t)$ которое придаёт системе управления (2) требуемые свойства (например, устойчивость или оптимальность в соответствии с некоторым критерием качества).

Пример

Рассмотрим контур системы управления ориентацией спутника. Вектор состояния в этом случае включает в себя параметры ориентации, задаваемые, например, кватернионом $q$ и угловую скорость $\boldsymbol{\omega}$.

Заметим, что уравнения (1) в данном случае сводятся к системе кинематических уравнений Пуассона и динамических уравнений Эйлера (система нелинейна):

$$\begin{cases}
    \dot{q} = \frac{1}{2}q \circ \boldsymbol{\omega} \\
    \dot{\boldsymbol{\omega}} = \mathbf{J}^{-1}\cdot(-\boldsymbol{\omega} \times \mathbf{J} \cdot \boldsymbol{\omega} + \mathbf{M}_{ext} + \mathbf{M}_{ctrl})
\end{cases}$$

Выход — это набор наблюдаемых или измеряемых параметров, связываемых (при помощи уравнения (3)) с состоянием системы. При наличии в контуре системы управления ориентацией звёздного датчика (ЗД) и датчика угловой скорости (ДУС) выход — это сразу $q$ и $\boldsymbol{\omega}$:
$$ЗД \rightarrow q$$
$$ДУС \rightarrow \boldsymbol{\omega}$$

В студенческих спутниках, как правило, находятся иные датчики: магнитометры, датчики Солнца, вертикали и др. В таком случае измеряются другие величины, которые можно конвертировать в состояние, но не само состояние. В линейных системах можно пользоваться уравнением выхода (3) для описания связи между $\mathbf{y}$ и $\mathbf{x}$.

Упражнение

Попробуйте записать (нелинейное) уравнение выхода для системы ориентации, в которой измеряется магнитное поле (магнитометром, в связанным со спутником осях) и сравнивается с бортовой моделью геомагнитного поля (выход которой представляется в некоторых инерциальных осях)

То же самое, но для комбинаций датчиков магнитометр + датчик угловой скорости, магнитометр + солнечные датчики, магнитометр + солнечные датчики + датчики угловой скорости

 Передаточные функции

Введём оператор дифференцирования

$$
    s = \frac{d}{dt}.
$$

Действие оператора, соответственно,

$$
    s \cdot x(t) = \dot{x}(t).
$$

Оператор, представленный в форме полинома:

$$
    p(s) = a_0 + a_1 s + a_2 s^2 + ... + a_k s^k,
$$
действует на вектор $\mathbf{x}$ как

$$
    p(s) \cdot x(t) = a_0 x(t) + a_1 \dot{x}(t) + ...
$$

Рассмотрим ранее введённые уравнения состояния (2). Из системы
$$\begin{cases}
    \dot{\mathbf{x}} = \mathbf{A} \cdot \mathbf{x} + \mathbf{B}\cdot \mathbf{u} + \mathbf{D}_1 \cdot \mathbf{v}\\
    \dot{\mathbf{x}}(t) = s\cdot \mathbf{x}(t)
\end{cases}$$
получим
$$
\mathbf{x} = (s \cdot \mathbf{I} - \mathbf{A})^{-1} \cdot(\mathbf{B}\cdot \mathbf{u} + \mathbf{D}_1\cdot\mathbf{v}).
$$
Подставляя получившееся выражение в уравнение выхода (3), получим
$$
 \mathbf{y} = \mathbf{C} \cdot (s \mathbf{I} - \mathbf{A})^{-1}\cdot \mathbf{B}\cdot \mathbf{u} + (\mathbf{C}\cdot(s \cdot\mathbf{I} - \mathbf{A})^{-1}\cdot\mathbf{D}_1 + \mathbf{D}_2)\cdot\mathbf{v}.
$$
Вводя новые обозначения, запишем последнее выражение в виде
$$
\mathbf{y} = G_{yu}(s)\cdot \mathbf{u} + G_{yv}(s) \cdot\mathbf{v},
$$
где

$G_{yu}(s) = \mathbf{C}\cdot(s \cdot\mathbf{I} - \mathbf{A})^{-1}\cdot \mathbf{B}$ — передаточная функция от управления к выходу,

$G_{yv}(s) = \mathbf{C}\cdot(s \cdot\mathbf{I} - \mathbf{A})^{-1}\cdot \mathbf{D}_1 + \mathbf{D}_2$ — передаточная функция от возмущения к выходу.

Таким образом, входной сигнал линейно преобразуется в выход.

Характеристический полином матрицы $\mathbf{A}$

$$
Q(s) = det(s\cdot \mathbf{I} - \mathbf{A})
$$
будет находиться в знаменателе при записи формул для вычисления обратных матриц. Нули характеристического полинома, т.е. собственные числа матрицы состояния $\mathbf{А}$ — полюса передаточных функций — характеризуют положения, при которых на ограниченное воздействие происходит неограниченный отклик.

Преобразование Лапласа

Преобразование Лапласа $\mathcal{L}:  x(t) \rightarrow X(s)$ т., что
$$
X(s) = \int_0^{\infty} x(t) e^{-st} dt,
$$
где $s$ - комплексная переменная.

Здесь
$x(t)$ называется оригиналом,
$X(s)$ — изображением.

Предъявляются следующие требования:

$x(t)$ определена на $[0; +\infty)$ и кусочно-дифференцируема,

$x(t) \equiv 0$ при $t < 0$,

$\exists \alpha > 0$ и $\exists M > 0:$ $|x(t)| < M e^{\alpha t}$ на $[0; +\infty)$.

Обратное преобразование Лапласа
$$
x(t) = \frac{1}{2 \pi i} \int_{a - i\infty}^{a + i \infty} x(s) e^{st}ds,
$$
где $Re(S) = a > \alpha$.

Свойства преобразования Лапласа:

Линейность: $\forall \alpha, \beta > 0 \hookrightarrow$
$$
\mathcal{L}\{\alpha x_1(t) + \beta x_2(t)\} = \alpha \mathcal{L}\{x_1(t)\} + \beta \mathcal{L}\{x_2(t)\}.
$$

Дифференцирование оригинала
$$
\mathcal{L}\{\dot{x}(t)\} = sX(s) - x(0),
$$

где $X(s) = \mathcal{L}\{x(t)\}$.

Интегрирование оригинала
$$
\mathcal{L}\{\int_0^\tau x(\tau) d\tau\} = \frac{X(s)}{s}
$$

Теорема о запаздывании:
$$
\mathcal{L}\{x(t - \tau)\} = e^{-\tau s}X(s)
$$

Теорема о свёртке:
$$
X_1(s) X_2(s) = \mathcal{L}\{\int_0^t x_1(\tau) x_2(t - \tau) d\tau\} = \mathcal{L}\{\int_0^t x_1(t - \tau) x_2(\tau) d\tau\}
$$

## Преобразование Лапласа с помощью символьных вычислений в Python

$$\mathcal{L}\{x(t)\}=\int_0^{\infty}{x(t) e^{-st}}dt$$

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

```python
# t, s и a - символьные переменные
t, s = sympy.symbols('t, s')
a = sympy.symbols('a', real = True, positive = True)
```

```python
# f - экспонента, заданная с помощью введённых ранее символов
f = sympy.exp(-a*t)
f
```

$\displaystyle e^{- a t}$

```python
# определим две функции, для прямого и обратного преобразования Лапласа

def L(f):
    return sympy.laplace_transform(f,t,s, noconds=True)

def invL(F):
    return sympy.inverse_laplace_transform(F, s, t)
```

```python
# вычислим прямое преобразование Лапласа от экспоненциальной функции f
F = L(f)
F
```

$\displaystyle \frac{1}{a + s}$

```python
# вычислим обратное преобразование Лапласа от получившегося изображения
# (заметьте, результат содержит фуункцию Хевисайда, равную нулю для всех t<0)
invL(F)
```

$\displaystyle e^{- a t} \theta\left(t\right)$

```python
# построим таблицу преобразований Лапласа для некоторых стандатных функций
# в этой ячейке определяется набор функций-оригиналов
omega, tau = sympy.symbols('omega, tau', real=True)
n = sympy.symbols('n', int=True, positive = True)
exp = sympy.exp
sin = sympy.sin
cos = sympy.cos
delta = sympy.DiracDelta
hv = sympy.Heaviside

functions = [1,
             delta(t),
             delta(t-a),
             hv(t),
             exp(-a*t),
             t*exp(-a*t),
             t**n*exp(-a*t),
             sin(omega*t),
             cos(omega*t)]

functions
```

    [1,
     DiracDelta(t),
     DiracDelta(-a + t),
     Heaviside(t),
     exp(-a*t),
     t*exp(-a*t),
     t**n*exp(-a*t),
     sin(omega*t),
     cos(omega*t)]

```python
# в этой ячейке определяется набор функций-изображений
Fs = [L(f) for f in functions]
Fs
```

    [1/s,
     1 - Heaviside(0),
     exp(-a*s),
     1/s,
     1/(a + s),
     (a + s)**(-2),
     (a + s)**(-n - 1)*gamma(n + 1),
     omega/(omega**2 + s**2),
     s/(omega**2 + s**2)]

```python
from pandas import DataFrame
```

```python
def makelatex(args):
    return ["$${}$$".format(sympy.latex(a)) for a in args]
```

```python
tblLaplace = DataFrame(list(zip(makelatex(functions),makelatex(Fs))))
tblLaplace.columns = ["Оригинал", "Изображение"]
tblLaplace
```

      Оригинал
      Изображение

      0
      $$1$$
      $$\frac{1}{s}$$

      1
      $$\delta\left(t\right)$$
      $$1 - \theta\left(0\right)$$

      2
      $$\delta\left(- a + t\right)$$
      $$e^{- a s}$$

      3
      $$\theta\left(t\right)$$
      $$\frac{1}{s}$$

      4
      $$e^{- a t}$$
      $$\frac{1}{a + s}$$

      5
      $$t e^{- a t}$$
      $$\frac{1}{\left(a + s\right)^{2}}$$

      6
      $$t^{n} e^{- a t}$$
      $$\left(a + s\right)^{- n - 1} \Gamma\left(n +...

      7
      $$\sin{\left(\omega t \right)}$$
      $$\frac{\omega}{\omega^{2} + s^{2}}$$

      8
      $$\cos{\left(\omega t \right)}$$
      $$\frac{s}{\omega^{2} + s^{2}}$$

# Упражнение
Вычислите преобразование Лапласа для $\delta$-функции вручную, сравните результат с полученным в таблице. Объясните разницу

# Описание и анализ одномерных систем

Для простоты рассмотрим систему с одномерным входом (единичная размерность вектора управляющих параметров $\mathbf{u}$) и одномерным выходом ($\mathbf{y}$). Такие системы называют в литературе SISO (single input - single output system), в отличие от MIMO (multiple inpupt - multiple output).

Система описывается дифференциальным уравнением
$$
a_nx^{(n)}(t) + a_{n-1}x^{(n-1)}(t) + ... + a_0 x(t) = b_mu^{(m)}(t) + ... + b_0 u(t). \tag{4}
$$

Подвергнем уравнение (4) преобразованию Лапласа при нулевых начальных условиях:
\begin{equation}\label{eq:poly}
Q_n(s) X(s) = P_m(s) U(s), \tag{5}
\end{equation}
$Q_n, P_m$ — полиномы от $s$.
Отсюда
$$
X(s) = \frac{P_m(s)}{Q_n(s)}U(s)
$$

Введём несколько определений:

Передаточной функцией (в изображениях Лапласа) $H(s)$ будем называть имеющее наименьший порядок отношение изображений её выходной и входной переменных при нулевых начальных условиях.

В силу линейности преобразования Лапласа, а также в силу свойства дифференцирования изображения полином $Q(s)$ является характеристическим полиномом однородного уравнения, соответствующего уравнению $\eqref{eq:poly}$.

Пользуясь уравнением $\eqref{eq:poly}$, можно записать
\begin{equation} \label{eq:transfer_fun}
X(s) = H(s)U(s). \tag{6}
\end{equation}

Весовой функцией $h(t)$ будем называть отклик системы на единичное импульсное воздействие ($\delta$-функцию) при нулевых начальных условиях.

Таким образом, весовая функция равна выходу $h(t) = x(t)$ системы при входе $u(t)=\delta(t)$. Преобразование Лапласа от входа $\mathcal{L}\{\delta(t)\} = 1$, поэтому в силу $\eqref{eq:transfer_fun}$
\begin{equation}\label{eq:delta_resp}
\mathcal{L}\{h(t)\} = H(s). \tag{7}
\end{equation}

Переходной функцией $\pi(t)$ будем называть отклик системы на единичное ступенчатое воздействие ($\theta(t)$ - функция Хевисайда) при нулевых начальных условиях.

Переходная функция равна выходу $h(t) = x(t)$ системы при входе $u(t)=\theta(t)$. Преобразование Лапласа от входа $\mathcal{L}\{\theta(t)\} = 1/s$, поэтому в силу $\eqref{eq:transfer_fun}$ и теоремы о дифференцировании оригинала
\begin{equation}\label{eq:step_resp}
\dot{\pi}(t) = h(t). \tag{8}
\end{equation}

## Пример

Пусть требуется определить переходную и весовые функции некоторого звена, передаточная функция которого задана как

$ H(s) = \frac{4}{s +2}$.

```python
H_s = 4/(s+2)
H_s
```

$\displaystyle \frac{4}{s + 2}$

#### Весовая функция $h(t)$ может быть определена как

```python
h_t = invL(H_s)
h_t
```

$\displaystyle 4 e^{- 2 t} \theta\left(t\right)$

#### В свою очередь,  переходная функция $\pi(t)$

```python
pi_t = invL(H_s/s)
pi_t
```

$\displaystyle 2 \theta\left(t\right) - 2 e^{- 2 t} \theta\left(t\right)$

### Построим графики найденных переходной и весовой функций

```python
pi_plot = sympy.plotting.plot(pi_t, (t, -1, 8), show = False)[0]
h_plot = sympy.plotting.plot(h_t, (t, -1, 8), show = False)[0]

pi_vs_t = pi_plot.get_points()
h_vs_t = h_plot.get_points()
```

```python
plt.rcParams['axes.labelsize'] = 16

fig = plt.figure(figsize=(10.5, 5))
ax1 = fig.add_subplot(121)
ax2 = fig.add_subplot(122)

# do subplot 1
ax1.plot(pi_vs_t[0], pi_vs_t[1], color='red', linewidth = 2)  # plot x**2 in green
ax1.set_xlim([-1, 8])
ax1.set_xlabel('t, c')
ax1.set_ylabel('$\pi(t)$')
ax1.set_title('Переходная функция')
ax1.grid()

# do subplot 2
ax2.plot(h_vs_t[0], h_vs_t[1], color='blue', linewidth = 2)  # plot x**3 in red
ax2.set_xlim([-1, 8])
ax2.set_xlabel('t, c')
ax2.set_ylabel('$h(t)$')
ax2.set_title('Весовая функция')
ax2.grid()

fig.subplots_adjust(wspace=0.5)
plt.show()
```

```python

```

Lecture Jupyter Notebooks/Lecture 3 State-Space Representation. Laplace Transform. Transfer Functions.ipynb


## Burgers Equation - Forward Euler/0-estimation
#### Parameter estimation for Burgers' Equation using Gaussian processes (Forward Euler scheme)

#### Problem Setup

$u_t + u u_{x} = \nu u_{x}$

$u(x,t) = \frac{x}{1+t}$ => We'd expect $\nu = 0$

$u_0(x) := u(x,0) = x$

$x \in [0, 1], t \in \{0, \tau \}$

Using the forward Euler scheme, the equation can be re-written as:

$\frac{u_n - u_{n-1}}{\tau} + u_{n-1} \frac{d}{dx}u_{n-1}  = \nu \frac{d^2}{dx}u_{n-1}$

and setting the factor $u_{n-1} = u_0(x) = x$ (no mean used! Should give a better result) to deal with the non-linearity:

$\tau \nu \frac{d^2}{dx}u_{n-1} - \tau x \frac{d}{dx}u_{n-1} + u_{n-1} = u_{n}$

Consider $u_{n-1}$ to be a Gaussian process.

$u_{n-1} \sim \mathcal{GP}(0, k_{uu}(x_i, x_j, \theta))$

And the linear operator:

$\mathcal{L}_x^\nu = \cdot + \tau \nu \frac{d}{dx}\cdot - \tau x \frac{d}{dx} \cdot$

so that

$\mathcal{L}_x^\nu u_{n-1} = u_n$

Problem at hand: estimate $\nu$ (should be $\nu = 0$ in the end).

For the sake of simplicity, take $u := u_{n-1}$ and $f := u_n$.

#### step 1: Simulate data

Take data points at $t = 0$ for $(u_{n-1})$ and $t = \tau$ for $(u_n)$, where $\tau$ is the time step.

```python
import numpy as np
import sympy as sp
import warnings
from scipy.optimize import minimize
import matplotlib.pyplot as plt
```

```python
tau = 0.001
def get_simulated_data(tau, alpha, beta):
    # Random numbers in [alpha, beta]
    x = np.random.rand(20)*(beta - alpha) + alpha
    y_u = x
    y_f = x/(1+tau)
    return (x, y_u, y_f)
```

#### Step 2:Evaluate kernels

$k_{nn}(x_i, x_j; \theta) = \theta exp(-\frac{1}{2l}(x_i-x_j)^2)$

```python
x_i, x_j, theta, l, nu = sp.symbols('x_i x_j theta l nu')
kuu_sym = theta*sp.exp(-1/(2*l)*((x_i - x_j)**2))
kuu_fn = sp.lambdify((x_i, x_j, theta, l), kuu_sym, "numpy")
def kuu(x, theta, l):
    k = np.zeros((x.size, x.size))
    for i in range(x.size):
        for j in range(x.size):
            k[i,j] = kuu_fn(x[i], x[j], theta, l)
    return k
```

$k_{ff}(x_i,x_j;\theta,\phi) \\
= \mathcal{L}_{x_i}^\nu \mathcal{L}_{x_j}^\nu k_{uu}(x_i, x_j; \theta) \\
= k_{uu} + \tau \nu \frac{d}{dx_i}k_{uu} - \tau x_i \frac{d}{dx_i}k_{uu} + \tau \nu \frac{d}{dx_j}k_{uu} + \tau^2 \nu^2 \frac{d}{dx_i} \frac{d}{dx_j}k_{uu} - \tau^2 \nu x_i\frac{d^2}{dx_i dx_j} k_{uu} - \tau x_j \frac{d}{dx_j}k_{uu} - \tau^2 \nu x_j \frac{d^2}{dx_i dx_j} k_{uu} + \tau^2 x_i x_j \frac{d^2}{dx_i dx_j}k_{uu}$

```python
kff_sym = kuu_sym \
        + tau*nu*sp.diff(kuu_sym, x_i) \
        - tau*x_i*sp.diff(kuu_sym, x_i) \
        + tau*nu*sp.diff(kuu_sym, x_j) \
        + tau**2*nu**2*sp.diff(kuu_sym, x_j, x_i) \
        - tau**2*nu*x_i*sp.diff(kuu_sym, x_j, x_i) \
        - tau*x_j*sp.diff(kuu_sym, x_j) \
        - tau**2*nu*x_j*sp.diff(kuu_sym, x_j, x_i) \
        + tau**2*x_i*x_j*sp.diff(kuu_sym, x_j, x_i)
kff_fn = sp.lambdify((x_i, x_j, theta, l, nu), kff_sym, "numpy")
def kff(x, theta, l, nu):
    k = np.zeros((x.size, x.size))
    for i in range(x.size):
        for j in range(x.size):
            k[i,j] = kff_fn(x[i], x[j], theta, l, nu)
    return k
```

$k_{fu}(x_i,x_j;\theta,\phi) \\
= \mathcal{L}_{x_i}^\nu k_{uu}(x_i, x_j; \theta) \\
= k_{uu} + \tau \nu \frac{d}{dx_i}k_{uu} - \tau x_i\frac{d}{dx_i}k_{uu}$

```python
kfu_sym = kuu_sym + tau*nu*sp.diff(kuu_sym, x_i) - tau*x_i*sp.diff(kuu_sym, x_i)
kfu_fn = sp.lambdify((x_i, x_j, theta, l, nu), kfu_sym, "numpy")
def kfu(x, theta, l, nu):
    k = np.zeros((x.size, x.size))
    for i in range(x.size):
        for j in range(x.size):
            k[i,j] = kfu_fn(x[i], x[j], theta, l, nu)
    return k
```

```python
def kuf(x, theta, l, nu):
    return kfu(x,theta, l, nu).T
```

#### Step 3: Compute NLML

```python
def nlml(params, x, y1, y2, s):
    theta_exp = np.exp(params[0])
    l_exp = np.exp(params[1])
    K = np.block([
        [kuu(x, theta_exp, l_exp) + s*np.identity(x.size), kuf(x, theta_exp, l_exp, params[2])],
        [kfu(x, theta_exp, l_exp, params[2]), kff(x, theta_exp, l_exp, params[2]) + s*np.identity(x.size)]
    ])
    y = np.concatenate((y1, y2))
    val = 0.5*(np.log(abs(np.linalg.det(K))) + np.mat(y) * np.linalg.inv(K) * np.mat(y).T)
    return val.item(0)
```

#### Step 4: Optimise hyperparameters

```python
%%timeit
(x, y_u, y_f) = get_simulated_data(tau, 0, 1)
m = minimize(nlml, np.random.rand(3), args=(x, y_u, y_f, 1e-7), method="Nelder-Mead")
```

    14.6 s ± 798 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

```python
results = []
for n in range(10):
    (x, y_u, y_f) = get_simulated_data(tau, n, n+1)
    warnings.filterwarnings("ignore")
    m = minimize(nlml, np.random.rand(3), args=(x, y_u, y_f, 1e-7), method="Nelder-Mead", options = {'maxiter' : 1000})
    if m.status == 0:
        results.append(m.x[2])
```

```python
results
```

    [0.000559966750333337,
     0.0016907407267387404,
     0.0024748815659894757,
     0.0036254494778221623,
     0.004338349097329195,
     0.0052319685083120114,
     0.006486625446119625,
     0.0072930009374944154,
     0.008210700508848801,
     0.009958709193246491]

```python
lin = np.linspace(1,len(results),len(results))
plt.plot(lin, results)
```

- The results of the first run (with an increasing length of the interval [alpha, beta]):

results = [0.0005310856122395808,
 0.0003428072035434465,
 0.0011232479569141262,
 0.0022636752135821313,
 0.0022616550320778746,
 -0.005003144177170458,
 -0.0123704500391504,
 0.0017474643621885146]

with an average error of 0.003,

where beta is given by the values on the x-axis.

- The result of the second run (with an increasing shift of the interval [beta - 1, beta]):

results =
[0.000559966750333337,
 0.0016907407267387404,
 0.0024748815659894757,
 0.0036254494778221623,
 0.004338349097329195,
 0.0052319685083120114,
 0.006486625446119625,
 0.0072930009374944154,
 0.008210700508848801,
 0.009958709193246491]

with an average error of 0.005,

where beta is given by the values on the x-axis.

```python

```


# Agricultural Pricing

## Objective and Prerequisites

Try this example to learn how to use mathematical optimization to tackle a common, but critical agricultural pricing problem:  Determining the prices and demand for a country’s dairy products in order to maximize total revenue derived from the sales of those products. You will learn how to model this problem as a quadratic optimization problem using the Gurobi Python API and solve it using the Gurobi Optimizer.

This model is example 21 from the fifth edition of Model Building in Mathematical Programming, by H. Paul Williams on pages 276-278 and 333-335.

This modeling example is at the intermediate level, where we assume that you know Python and are familiar with the Gurobi Python API. In addition, you should have some knowledge about building mathematical optimization models.

**Download the Repository**

You can download the repository containing this and other examples by clicking [here](https://github.com/Gurobi/modeling-examples/archive/master.zip).

**Gurobi License**

In order to run this Jupyter Notebook properly, you must have a Gurobi license. If you do not have one, you can request an [evaluation license](https://www.gurobi.com/downloads/request-an-evaluation-license/?utm_source=3PW&utm_medium=OT&utm_campaign=WW-MU-EDU-OR-O_LEA-PR_NO-Q3_FY20_WW_JPME_AGRICULTURAL_PRICING_COM_EVAL_GitHub&utm_term=Agricultural%20Pricing&utm_content=C_JPM) as a *commercial user*, or download a [free license](https://www.gurobi.com/academia/academic-program-and-licenses/?utm_source=3PW&utm_medium=OT&utm_campaign=WW-MU-EDU-OR-O_LEA-PR_NO-Q3_FY20_WW_JPME_AGRICULTURAL_PRICING_ACADEMIC_EVAL_GitHub&utm_term=Agricultural%20Pricing&utm_content=C_JPM) as an *academic user*.

## Problem Description

The government of a country wants to decide what prices should be charged for its dairy products: milk, butter, and cheese. All these products
are produced
(directly or indirectly) from the country’s raw milk production operations. This raw milk is divided into two main components: fat and dry matter. After subtracting the quantities of fat and dry matter, which are used for making products for export or consumption on the farms, there is a total yearly availability of 600,000 tons of fat and 750,000 tons of dry matter. This is all available for producing milk, butter and two kinds of cheese for domestic consumption. The percentage composition of the products are given in the following table:

| Composition | Fat (%) | Dry matter (%) |
| --- | --- | --- |
| Milk | 4 | 9 |
| Butter | 80 | 2 |
| Cheese 1 | 35 | 30 |
| Cheese 2 | 25 | 40 |

The table below shows last year's domestic consumption (demand) and prices for the dairy products:

| Dairy
 products | Milk | Butter | Cheese 1 | Cheese 2 |
| --- | --- | --- | --- | --- |
| Demand (1000 tons) | 4.82 | 0.32 |  0.21 | 0.07 |
| Price (dollars/ton) | 297 | 720 | 1050 | 815 |

The elasticities and cross-elasticities are given in the table below:

| Milk | Butter | Cheese 1 | Cheese 2 | Cheese 1 to
 Cheese 2 |  Cheese 2 to
 Cheese 1 |
| --- | --- | --- | --- | --- |  --- |
| 0.4 | 2.7 | 1.1 |  0.4 | 0.1 |  0.4 |

The price index cannot be raised higher than last year.This constraint establishes that the new prices must be such that the total cost of last year’s consumption would not be increased.
Last year's price index is 1.939 (measured in thousand dollars).

The goal is to determine what prices and demand  maximizes the total revenue.

## Model Formulation

### Sets and Indices

$d \in \text{Dairy}=\{\text{milk}, \text{butter}, \text{cheese1}, \text{cheese2} \}$

$c \in \text{Components}=\{\text{fat}, \text{dry_matter} \}$

### Parameters

$\text{capacity}_{c} \in \mathbb{R}^+$: Yearly availability of component $c$ (1000 tons).

$\text{qtyper}_{c,d} \in [0,1]$: Percentage of component $c$ in dairy product $d$.

$\text{consumption}_{d} \in \mathbb{R}^+$: Last year domestic consumption of dairy product  $d$ (1000 tons).

$\text{price}_{d} \in \mathbb{R}^+$: Last year price of dairy product $d$ (dollars/1000 tons).

$\text{elasticity}_{d} \in \mathbb{R}^+$: Last year price elasticity of domestic consumption of dairy product $d$.

$\text{elasticity12} \in \mathbb{R}^+$: Last year price cross-elasticity of domestic consumption of cheese 1 and cheese 2.

$\text{elasticity21} \in \mathbb{R}^+$: Last year price cross-elasticity of domestic consumption of cheese 2 and cheese 1.

$\text{prcIndex} \in \mathbb{R}^+$: Price index reflecting last year total consumption cost.

### Decision Variables

$\text{p}_{d} \in \mathbb{R}^+$: Price of dairy product $d$ (dollars/1000 tons).

$\text{q}_{d} \in \mathbb{R}^+$: Demand of dairy product $d$ (1000 tons).

### Constraints

**Capacity**: The limited availabilities of fat and dry matter are enforced by the following constraints.

\begin{equation}
\sum_{d \in \text{Dairy}}{\text{qtyper}_{c,d}*\text{q}_{d} } \leq \text{capacity}_{c} \quad \forall c \in \text{Components}
\end{equation}

**Price index**: This constraint establishes that the new prices must be such that the total cost of last year’s consumption would not be increased.

\begin{equation}
\sum_{d \in \text{Dairy}}{\text{consumption}_{d}*\text{p}_{d} } \leq \text{prcIndex}
\end{equation}

**Elasticities**: The demand variables $q_{d}$ are related to the price variables $p_{d}$  through the price elasticities relationships. We approximate the elasticities with linear relationships.

Milk elasticity.
$$
(\text{q}_{milk} - \text{consumption}_{milk})/\text{consumption}_{milk}) = -\text{elasticity}_{milk}*(\text{p}_{milk} - \text{price}_{milk})/\text{price}_{milk})
$$

Butter elasticity.
$$
(\text{q}_{butter} - \text{consumption}_{butter})/\text{consumption}_{butter}) = -\text{elasticity}_{butter}*(\text{p}_{butter} - \text{price}_{butter})/\text{price}_{butter})
$$

Cheese 1 elasticity.
$$
(\text{q}_{cheese1} - \text{consumption}_{cheese1})/\text{consumption}_{cheese1}) = -\text{elasticity}_{cheese1}*(\text{p}_{cheese1} - \text{price}_{cheese1})/\text{price}_{cheese1})
$$

$$
+ elasticity12*(\text{p}_{cheese2} - \text{price}_{cheese2})/\text{price}_{cheese2})
$$

Cheese 2 elasticity.
$$
(\text{q}_{cheese2} - \text{consumption}_{cheese2})/\text{consumption}_{cheese2}) = -\text{elasticity}_{cheese2}*(\text{p}_{cheese2} - \text{price}_{cheese2})/\text{price}_{cheese2})
$$

$$
+ elasticity21*(\text{p}_{cheese1} - \text{price}_{cheese1})/\text{price}_{cheese1})
$$

### Objective Function

**Revenue**: The objective is to maximize total revenue.

\begin{equation}
\text{Maximize} \quad \sum_{d \in \text{Dairy}}{\text{q}_{d}*\text{p}_{d} }
\end{equation}

---
## Python Implementation

We import the Gurobi Python Module and other Python libraries.

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

import gurobipy as gp
from gurobipy import GRB

# tested with Python 3.7.0 & Gurobi 9.0
```

## Input data

We define all the input data for the model.

```python
# List of dairy products.

dairy = ['milk', 'butter', 'cheese1', 'cheese2']

components = ['fat', 'dryMatter']

# Create a dictionary to capture the percentage composition of the products.

cd, qtyper = gp.multidict({
    ('fat','milk'): 0.04,
    ('fat','butter'): 0.8,
    ('fat','cheese1'): 0.35,
    ('fat','cheese2'): 0.25,
    ('dryMatter','milk'): 0.09,
    ('dryMatter','butter'): 0.02,
    ('dryMatter','cheese1'): 0.3,
    ('dryMatter','cheese2'): 0.4
})

# Create a dictionary to capture the yearly availability of components (1000 tons).

components, capacity = gp.multidict({
    ('fat'): 600,
    ('dryMatter'): 750
})

# Create a dictionary to capture last year's domestic consumption and prices

dairy, consumption, price, elasticity = gp.multidict({
    ('milk'): [4.82, 0.297, 0.4],
    ('butter'): [0.32, 0.72, 2.7],
    ('cheese1'): [0.21, 1.05, 1.1],
    ('cheese2'): [0.07, 0.815, 0.4]
})

elasticity12 = 0.1
elasticity21 = 0.4

priceIndex = 1.939
```

## Model Deployment

We create a model and the variables. The decision variables of this model are the prices and demand for the dairy products.

Solving bilinear problems with Gurobi is as easy as configuring the global parameter `nonConvex`, and setting this parameter to the value of 2.

```python
model = gp.Model('AgriculturalPricing')

# Set global parameters.
model.params.nonConvex = 2

# Quantity of dairy products.
qvar = model.addVars(dairy, name="qvar")

# Price of dairy products.
pvar = model.addVars(dairy, name="pvar")

```

    Using license file c:\gurobi\gurobi.lic
    Changed value of parameter nonConvex to 2
       Prev: -1  Min: -1  Max: 2  Default: -1

The limited availabilities of fat and dry matter are enforced by the following constraints.

```python
# Capacity constraint.

fatCap = model.addConstrs( (gp.quicksum(qtyper[c,d]*qvar[d] for d in dairy) <= capacity[c] for c in components  ),
                          name='fatCap')
```

This constraint ensures that the new prices must be such that the total cost of last year’s consumption would not be increased.

```python
# Price index constraint.

priceIndex = model.addConstr( (gp.quicksum(consumption[d]*pvar[d] for d in dairy) <= priceIndex ), name='priceIndex')
```

The demand variables are related to the price variables  through the price elasticities relationships. We approximate the elasticities with linear relationships.

```python
# Elasticity constraints

elasMilk = model.addConstr( (qvar['milk']-consumption['milk'])/consumption['milk']
                           == -elasticity['milk']*(pvar['milk']-price['milk'])/price['milk'], name='elasMilk')

elasButter = model.addConstr( (qvar['butter']-consumption['butter'])/consumption['butter']
                           == -elasticity['butter']*(pvar['butter']-price['butter'])/price['butter'], name='elasButter')

elasCheese1 = model.addConstr( (qvar['cheese1']-consumption['cheese1'])/consumption['cheese1']
                           == -elasticity['cheese1']*(pvar['cheese1']-price['cheese1'])/price['cheese1']
                              +elasticity12*(pvar['cheese2']-price['cheese2'])/price['cheese2'] , name='elasCheese1')

elasCheese2 = model.addConstr( (qvar['cheese2']-consumption['cheese2'])/consumption['cheese2']
                           == -elasticity['cheese2']*(pvar['cheese2']-price['cheese2'])/price['cheese2']
                              +elasticity21*(pvar['cheese1']-price['cheese1'])/price['cheese1'] , name='elasCheese2')
```

The objective function is to maximize revenue.

```python
# Quadratic objective function.

obj = gp.quicksum(qvar[d]*pvar[d] for d in dairy)

model.setObjective(obj, GRB.MAXIMIZE)
```

```python
# Verify model formulation

model.write('AgriculturalPricing.lp')

# Run optimization engine

model.optimize()
```

    Gurobi Optimizer version 9.1.0 build v9.1.0rc0 (win64)
    Thread count: 4 physical cores, 8 logical processors, using up to 8 threads
    Optimize a model with 7 rows, 8 columns and 22 nonzeros
    Model fingerprint: 0x4fffdf2e
    Model has 4 quadratic objective terms
    Coefficient statistics:
      Matrix range     [2e-02, 1e+01]
      Objective range  [0e+00, 0e+00]
      QObjective range [2e+00, 2e+00]
      Bounds range     [0e+00, 0e+00]
      RHS range        [1e+00, 8e+02]
    Presolve removed 2 rows and 0 columns

    Continuous model is non-convex -- solving as a MIP.

    Presolve removed 2 rows and 0 columns
    Presolve time: 0.00s
    Presolved: 14 rows, 13 columns, 36 nonzeros
    Presolved model has 4 bilinear constraint(s)
    Variable types: 13 continuous, 0 integer (0 binary)

    Root relaxation: objective 2.791205e+00, 11 iterations, 0.00 seconds

        Nodes    |    Current Node    |     Objective Bounds      |     Work
     Expl Unexpl |  Obj  Depth IntInf | Incumbent    BestBd   Gap | It/Node Time

         0     0    2.79121    0    4          -    2.79121      -     -    0s
    H    0     0                       2.0422947    2.79121  36.7%     -    0s
         0     0    2.23682    0    4    2.04229    2.23682  9.53%     -    0s
         0     0    2.18215    0    4    2.04229    2.18215  6.85%     -    0s
    H    0     0                       2.0596849    2.18215  5.95%     -    0s
         0     0    2.17036    0    4    2.05968    2.17036  5.37%     -    0s
         0     0    2.16917    0    4    2.05968    2.16917  5.32%     -    0s
         0     0    2.16441    0    4    2.05968    2.16441  5.08%     -    0s
         0     0    2.16252    0    4    2.05968    2.16252  4.99%     -    0s
         0     0    2.15497    0    4    2.05968    2.15497  4.63%     -    0s
    H    0     0                       2.0598300    2.15497  4.62%     -    0s
         0     0    2.15391    0    4    2.05983    2.15391  4.57%     -    0s
    H    0     0                       2.0626181    2.15391  4.43%     -    0s
    H    0     0                       2.0653760    2.15391  4.29%     -    0s
    H    0     0                       2.0656667    2.15391  4.27%     -    0s
         0     2    2.15391    0    4    2.06567    2.15391  4.27%     -    0s
    *   85    35              18       2.0658550    2.07220  0.31%   1.6    0s
    *  103    33              16       2.0658649    2.06832  0.12%   1.6    0s
    *  139    49              18       2.0663278    2.06781  0.07%   1.5    0s
    *  161    53              19       2.0663286    2.06781  0.07%   1.4    0s
    *  164    53              18       2.0663408    2.06781  0.07%   1.4    0s
    *  165    53              18       2.0663409    2.06781  0.07%   1.4    0s
    *  209    59              21       2.0663474    2.06758  0.06%   1.4    0s
    *  210    59              21       2.0663482    2.06758  0.06%   1.4    0s
    *  211    59              20       2.0663508    2.06758  0.06%   1.4    0s
    *  226    53              27       2.0663906    2.06712  0.04%   1.3    0s
    *  339    58              24       2.0663946    2.06654  0.01%   1.2    0s
    *  341    58              25       2.0663954    2.06654  0.01%   1.2    0s
    *  368    58              22       2.0663973    2.06649  0.00%   1.2    0s
    *  371    58              23       2.0663990    2.06649  0.00%   1.2    0s

    Cutting planes:
      RLT: 9

    Explored 397 nodes (484 simplex iterations) in 0.11 seconds
    Thread count was 8 (of 8 available processors)

    Solution count 10: 2.0664 2.0664 2.0664 ... 2.06634

    Optimal solution found (tolerance 1.00e-04)
    Best objective 2.066398260370e+00, best bound 2.066493116553e+00, gap 0.0046%

## Analysis

The table below shows the price (dollars/ton) and demand (tons) at equilibrium, for each dairy product. The total revenue generated is $\$ 2,066,398,260$.

```python
# Output Report
price_demand = pd.DataFrame(columns=["Products", "Price", "Demand"])
for d in dairy:
    price_demand = price_demand.append({"Products": d, "Price": '${:,.2f}'.format(round(1000*pvar[d].x)), "Demand": '{:,.2f}'.format(round(1e6*qvar[d].x))}, ignore_index=True)
price_demand.index=[''] * len(price_demand)
price_demand
```

      Products
      Price
      Demand

      milk
      $321.00
      4,661,067.00

      butter
      $422.00
      677,334.00

      cheese1
      $839.00
      264,129.00

      cheese2
      $1,116.00
      54,042.00

---
## References

H. Paul Williams, Model Building in Mathematical Programming, fifth edition.

Copyright © 2020 Gurobi Optimization, LLC

```python

```


```python
%matplotlib inline
```

Linear Regression with Regularization
=====================================

Regularization is a way to prevent overfitting and allows the model to
generalize better. We'll cover the *Ridge* and *Lasso* regression here.

The Need for Regularization
---------------------------

Unlike polynomial fitting, it's hard to imagine how linear regression
can overfit the data, since it's just a single line (or a hyperplane).
One situation is that features are **correlated** or redundant.

Suppose there are two features, both are exactly the same, our predicted
hyperplane will be in this format:

\begin{align}\hat{y} = w_0 + w_1x_1 + w_2x_2\end{align}

and the true values of $x_2$ is almost the same as $x_1$ (or
with some multiplicative factor and noise). Then, it's best to just drop
$w_2x_2$ term and use:

\begin{align}\hat{y} = w_0 + w_1x_1\end{align}

to fit the data. This is a simpler model.

But we don't know whether $x_1$ and $x_2$ is **actually**
redundant or not, at least with bare eyes, and we don't want to manually
drop a parameter just because we feel like it. We want to model to learn
to do this itself, that is, to *prefer a simpler model that fits the
data well enough*.

To do this, we add a *penalty term* to our loss function. Two common
penalty terms are L2 and L1 norm of $w$.

L2 and L1 Penalty
-----------------

0. No Penalty (or Linear)
~~~~~~~~~~~~~~~~~~~~~~~~~

This is linear regression without any regularization (from `previous
article </blog_content/linear_regression/linear_regression_tutorial.html#writing-sse-loss-in-matrix-notation>`__):

\begin{align}L(w) = \sum_{i=1}^{n} \left( y^i - wx^i \right)^2\end{align}

1. L2 Penalty (or Ridge)
~~~~~~~~~~~~~~~~~~~~~~~~

We can add the **L2 penalty term** to it, and this is called **L2
regularization**.:

\begin{align}L(w) = \sum_{i=1}^{n} \left( y^i - wx^i \right)^2 + \lambda\sum_{j=0}^{d}w_j^2\end{align}

This is called L2 penalty just because it's a L2-norm of $w$. In
fancy term, this whole loss function is also known as **Ridge
regression**.

Let's see what's going on. Loss function is something we **minimize**.
Any terms that we add to it, we also want it to be minimized (that's why
it's called *penalty term*). The above means we want $w$ that fits
the data well (first term), but we also want the values of $w$ to
be small as possible (second term). The lambda ($\lambda$) is
there to adjust how much to penalize $w$. Note that ``sklearn``
refers to this as alpha ($\alpha$) instead, but whatever.

It's tricky to know the appropriate value for lambda. You just have to
try them out, in exponential range (0.01, 0.1, 1, 10, etc), then select
the one that has the lowest loss on validation set, or doing k-fold
cross validation.

Setting $\lambda$ to be very low means we don't penalize the
complex model much. Setting it to $0$ is the original linear
regression. Setting it high means we strongly prefer simpler model, at
the cost of how well it fits the data.

Closed-form solution of Ridge
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

It's not hard to find a closed-form solution for Ridge, first write the
loss function in matrix notation:

\begin{align}L(w) = {\left\lVert y - Xw \right\rVert}^2 + \lambda{\left\lVert w \right\rVert}_2^2\end{align}

Then the gradient is:

\begin{align}\nabla L_w = -2X^T(y-Xw) + 2\lambda w\end{align}

Setting to zero and solve:

\begin{align}\begin{align}
   0 &= -2X^T(y-Xw) + 2\lambda w \\
   &= X^T(y-Xw) - \lambda w    \\
   &= X^Ty - X^TXw - \lambda w \\
   &= X^Ty - (X^TX + \lambda I_d) w
   \end{align}\end{align}

Move that to other side and we get a closed-form solution:

\begin{align}\begin{align}
   (X^TX + \lambda I_d) w &= X^Ty    \\
   w &= (X^TX + \lambda I_d)^{-1}X^Ty
   \end{align}\end{align}

which is almost the same as linear regression without regularization.

2. L1 Penalty (or Lasso)
~~~~~~~~~~~~~~~~~~~~~~~~

As you might guess, you can also use L1-norm for **L1 regularization**:

\begin{align}L(w) = \sum_{i=1}^{n} \left( y^i - wx^i \right)^2 + \lambda\sum_{j=0}^{d}\left|w_j\right|\end{align}

Again, in fancy term, this loss function is also known as **Lasso
regression**. Using matrix notation:

\begin{align}L(w) = {\left\lVert y - Xw \right\rVert}^2 + \lambda{\left\lVert w \right\rVert}_1\end{align}

It's more complex to get a closed-form solution for this, so we'll leave
it here.

Visualizing the Loss Surface with Regularization
------------------------------------------------

Let's see what these penalty terms mean geometrically.

L2 loss surface
~~~~~~~~~~~~~~~

.. figure:: imgs/img_l2_surface.png
   :alt: img\_l2\_surface

   img\_l2\_surface

This simply follows the 3D equation:

\begin{align}L(w) = {\left\lVert w \right\rVert}_2^2 = w_0^2 + w_1^2\end{align}

The center of the bowl is lowest, since ``w = [0,0]``, but that is not
even a line and it won't predict anything useful.

L2 loss surface under different lambdas
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

When you multiply the L2 norm function with lambda,
$L(w) = \lambda(w_0^2 + w_1^2)$, the width of the bowl changes.
The lowest (and flattest) one has lambda of 0.25, which you can see it
penalizes The two subsequent ones has lambdas of 0.5 and 1.0.

.. figure:: imgs/img_l2_surface_lambdas.png
   :alt: img\_l2\_surface\_lambdas

   img\_l2\_surface\_lambdas

L1 loss surface
~~~~~~~~~~~~~~~

Below is the loss surface of L1 penalty:

.. figure:: imgs/img_l1_surface.png
   :alt: img\_l1\_surface

   img\_l1\_surface

Similarly the equation is
$L(w) = \lambda(\left| w_0 \right| + \left| w_1 \right|)$.

Contour of different penalty terms
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

If the L2 norm is 1, you get a unit circle ($w_0^2 + w_1^2 = 1$).
In the same manner, you get "unit" shapes in other norms:

.. figure:: imgs/img_penalty_contours.png
   :alt: img\_penalty\_contours

   img\_penalty\_contours

**When you walk along these lines, you get the same loss, which is 1**

These shapes can hint us different behaviors of each norm, which brings
us to the next question.

Which one to use, L1 or L2?
---------------------------

What's the point of using different penalty terms, as it seems like both
try to push down the size of $w$.

**Turns out L1 penalty tends to produce sparse solutions**. This means
many entries in $w$ are zeros. This is good if you want the model
to be simple and compact. Why is that?

Geometrical Explanation
~~~~~~~~~~~~~~~~~~~~~~~

*Note: these figures are generated with unusually high lambda to
exaggerate the plot*

First let's bring both linear regression and penalty loss surface
together (left), and recall that we want to find the **minimum loss when
both surfaces are summed up** (right):

.. figure:: imgs/img_ridge_regression.png
   :alt: ridge

   ridge

Ridge regression is like finding the middle point where the loss of a
sum between linear regression and L2 penalty loss is lowest:

.. figure:: imgs/img_ridge_sol_30.png
   :alt: ridge\_solution

   ridge\_solution

You can imagine starting with the linear regression solution (red point)
where the loss is the lowest, then you move towards the origin (blue
point), where the penalty loss is lowest. **The more lambda you set, the
more you'll be drawn towards the origin, since you penalize the values
of $w_i$ more** so it wants to get to where they're all zeros:

.. figure:: imgs/img_ridge_sol_60.png
   :alt: ridge\_solution

   ridge\_solution

Since the loss surfaces of linear regression and L2 norm are both
ellipsoid, the solution found for Ridge regression **tends to be
directly between both solutions**. Notice how the summed ellipsoid is
still right in the middle.

--------------

For Lasso:

.. figure:: imgs/img_lasso_regression.png
   :alt: lasso

   lasso

And this is the Lasso solution for lambda = 30 and 60:

.. figure:: imgs/img_lasso_sol_30.png
   :alt: lasso\_solution

   lasso\_solution

.. figure:: imgs/img_lasso_sol_60.png
   :alt: lasso\_solution

   lasso\_solution

Notice that the ellipsoid of linear regression **approaches, and finally
hits a corner of L1 loss**, and will always stay at that corner. What
does a corner of L1 norm means in this situation? It means
$w_1 = 0$.

Again, this is because the contour lines **at the same loss value** of
L2 norm reaches out much farther than L1 norm:

.. figure:: imgs/img_l1_vs_l2_contour.png
   :alt: img\_l1\_vs\_l2\_contour

   img\_l1\_vs\_l2\_contour

If the linear regression finds an optimal contact point along the L2
circle, then it will stop since there's no use to move sideways where
the loss is usually higher. However, with L1 penalty, it can drift
toward a corner, because it's **the same loss along the line** anyway (I
mean, why not?) and thus is exploited, if the opportunity arises.


A Lower Bound on the Dark Matter Mass from BBN

We want to extract a lower bound on the dark matter mass in various scenarios by running a $\chi^2$ analysis on the computed element abundances using *PrimiCosmo* and *PrimiNuc*.

Statistical Assessment

In order to set constraints ont he dark matter mass, we follow the PDG recommended primordial abundances for Helium and Deuterium, that at 1$\sigma$ read,

$$Y_p = 0.245 \pm 0.003, \quad \mathrm{D}/\mathrm{H} = (2.569 \pm 0.027) \times 10^{-5}$$

In addition, we account for theoretical uncertainties in their predicted abundances resulting from uncertainties in the neutron lifetime and mainly from the ${}^2{\rm H} + p \leftrightarrow n +{}^3{\rm He}$ reaction which explicitly read:
\begin{align}
\sigma(Y_{\rm P})^{\rm Theo} 	        		&= 0.00017 \\
\sigma({\rm D/H}|_{\rm P} )^{\rm Theo}   	&= 0.036 \times 10^{-5}
\end{align}
With this we can write down the effective BBN $\chi^2$ as
\begin{align}
\chi_{\rm BBN}^2 = \frac{\left[Y_{\rm P} - Y_{\rm P}^{\rm Exp}\right]^2}{\sigma_{Y_{\rm P}}^2|{}^{\rm Theo} + \sigma_{Y_{\rm P}}^2|{}^{\rm Exp}} + \frac{\left[{\rm D/H}|_{\rm P} - {\rm D/H}|_{\rm P}^{\rm Exp}\right]^2}{\sigma_{{\rm D/H}|_{\rm P}}^2|{}^{\rm Theo} + \sigma_{{\rm D/H}|_{\rm P}}^2|{}^{\rm Exp}}
\end{align}
Similarly, we use the latest CMB observations by the Planck satellite to construct a CMB likelihood from their 2018 TT+TE+EE+lowE analysis, that reads:
\begin{align}
\chi_{\rm CMB}^2  = \left(\Theta - \Theta_{\rm obs}\right)^T \,  \Sigma_{\rm CMB}^{-1} \,\left(\Theta - \Theta_{\rm obs}\right)
\end{align}
where $\Theta = (\omega_b h^2,\,N_{\rm eff},\,Y_{\rm P})$, and
\begin{align}
\Sigma_{\rm CMB} = \Delta^T \, \left(
\begin{array}{ccc}
1 & \rho_{12} & \rho_{13}  \\
 \rho_{12}  & 1 &\rho_{23} \\
 \rho_{13}  & \rho_{23} &1 \\
\end{array}
\right) \, \Delta
\end{align}
and:
\begin{align}
\Theta_{\rm obs} &= (0.00223,\, 2.89,\, 0.246) \\
\Delta &= (0.00022,\, 0.31,\, 0.018) \\
\rho_{12},\,\rho_{13},\,\rho_{23} &= 0.40,\, 0.18,\, -0.69
\end{align}

BBN Constraints

We begin with the constraints from Big Bang Nucleosynthesis i.e. by computing $\chi^2_{\mathrm{BBN}}$. We take the **neutrinophilic complex scalar** as the test example. First we load the data and perform a little bit of investigation regarding the distributions and dependence on the dark matter mass.

```python
from utils import get_data
from utils import plot_distributions, plot_abundances, plot_chisq_distribution, plot_mchi_omegab_contours
from utils import plot_joint_mchi_omegab, plot_deltachisq
from utils import get_chisq_grid, get_mass_omegab_grid, get_masses
from utils import chisq
from utils import save_results

import warnings
warnings.filterwarnings('ignore')
```

```python
scenario = 'Nu_Zp'
filename = scenario + '.txt'
data = get_data(filename)
print('Loaded data from: {}\nLength: {}'.format(filename, len(data['mass'])))

plot_distributions(data, scenario)
plot_abundances(data, scenario)
```

Shown in red are the 1$\sigma$ error bounds around the central value. Now we compute the $\chi^2$ using the definition given above,

\begin{align}
\chi_{\rm BBN}^2 = \frac{\left[Y_{\rm P} - Y_{\rm P}^{\rm Exp}\right]^2}{\sigma_{Y_{\rm P}}^2|{}^{\rm Theo} + \sigma_{Y_{\rm P}}^2|{}^{\rm Exp}} + \frac{\left[{\rm D/H}|_{\rm P} - {\rm D/H}|_{\rm P}^{\rm Exp}\right]^2}{\sigma_{{\rm D/H}|_{\rm P}}^2|{}^{\rm Theo} + \sigma_{{\rm D/H}|_{\rm P}}^2|{}^{\rm Exp}}
\end{align}

Furthermore, we take the $1 \sigma$, $2 \sigma$, etc. critical values to be,

| Confidence Level | Critical Value ($\nu = 1$) | Critical Value ($\nu = 2$)|
| --- | --- | --- |
| 1$\sigma$ |  1.00 |  2.30 |
| 2$\sigma$ |  4.00 |  6.18 |
| 3$\sigma$ |  9.00 | 11.83 |
| 4$\sigma$ | 16.00 | 19.33 |
| 5$\sigma$ | 25.00 | 28.74 |

```python
plot_chisq_distribution(data, scenario)
```

```python
plot_mchi_omegab_contours(data, scenario, 'BBN')
plot_mchi_omegab_contours(data, scenario, 'CMB')
plot_mchi_omegab_contours(data, scenario, 'BBN+CMB')
```

```python
plot_joint_mchi_omegab(data, scenario)
```

Marginalising over $\Omega_b h^2$

The next step is to marginalise over the baryon density. This is achieved as follows: we define the marginalised $\chi^2$ as a function of the dark matter mass,

$$\tilde{\chi}^2(m_\chi) := \min_{\Omega_b h^2}{\chi^2(m_\chi, \Omega_b h^2)}$$

To work directly with confidence levels, we then subtract the minimum value over all masses,

$$\Delta \tilde{\chi}^2_{\mathrm{BBN}} := \tilde{\chi}^2(m_\chi) - \min_{m_\chi}{\tilde{\chi}^2(m_\chi)}$$

```python
plot_deltachisq(data, scenario, zoom=False)
```

```python
plot_deltachisq(data, scenario, zoom=True)
```

We collect the results to find the lowest $m_\chi$ for each confidence level so as to set the desired bounds.

```python
save_results(data, scenario, x0=10.0, save=True)
```


# Clase 3b: Perfil de Yukovski

_Aunque no te lo creas, __con lo que hemos visto hasta ahora eres capaz de hacer grandes cosas__. Vale sí, un perfil de Yukovski no es gran cosa aerodinámicamente, pero si lo hacemos en Python... Echa un vistazo a la figura ¿no está mal, no? algo así intentaremos conseguir al final de esta clase._

_Como no se trata de aprender (o reaprender) aerodinámica, te daremos las funciones matemáticas y los pasos a seguir así como la estructura del programa. Tú sólo tienes que preocuparte de programar cada bloque. Puedes leer en detalle todo lo relativo a la aerodinámica en el libro  Aerodinámica básica de Meseguer Ruiz, J., Sanz Andrés, A. (Editorial Garceta)._

## 1. Importamos paquetes.

Lo primero es lo primero, importemos los paquetes:

```python
# Recuerda, utilizaremos arrays y pintaremos gráficas.

```

## 2. Parámetros del problema

######
__Fuente:__ _Aerodinámica básica, Meseguer Ruiz, J., Sanz Andrés, A._

La transformación de Yukovski es: $$\tau=t+\frac{a^{2}}{t}$$

Los parámetros del problema son los del siguiente bloque, puedes cambiarlos más adelante:

```python
# Datos para el perfil de Yukovski

# Parámetro de la transformación de Yukovski
a = 1

# Centro de la circunferencia
landa = 0.2    # coordenada x (en valor absoluto)
delta = 0.3     # coordenada y

t0 = a * (-landa + delta * 1j)     # centro: plano complejo

# Valor del radio de la circunferencia
R = a * np.sqrt((1 + landa)**2 + delta**2)

# Ángulo de ataque corriente incidente
alfa_grados = 0
alfa = np.deg2rad(alfa_grados)

#Velocidad de la corriente incidente
U = 1
```

## 3. Perfil de Yukoski a partir de una circunferencia.

### Función transformación de Yukovski

__Se trata de definir una función que realice la transformación de Yukovski.__ Esta función recibirá el parámetro de la transformación, $a$ y el punto del plano complejo $t$. Devolverá el valor $\tau$, punto del plano complejo en el que se transforma $t$.

```python
def transf_yukovski(a, t):
    """Dado el punto t (complejo) y el parámetro a
    a de la transformación proporciona el punto
    tau (complejo) en el que se transforma t."""
```

```python
#comprobamos que la función está bien programada
#puntos del eje real siguen siendo del eje real
err_message = "La transformación de Yukovski no devuelve un resultado correcto"
np.testing.assert_equal(transf_yukovski(1, 1+0j), 2+0j, err_message)
```

### Circunferencia

Ahora queremos transformar la circunferencia de radio $R$ con centro en $t_0$ usando la función anterior:

1. __Creamos `N` puntos de la circunferencia__ de modo que __en `Xc` estén las coordenadas $x$ y en `Yc` estén las coordenadas $y$__ de los puntos que la forman. Controla el número de puntos mediante un parámetro que se llame `N_perfil`.
    $$X_c = real(t_0) + R·cos(\theta)$$
    $$Y_c = imag(t_0) + R·sin(\theta)$$
2. Una vez hayas obtenido los dos arrays `Xc` e `Yc`, __píntalos mediante un `scatter`__ para comprobar que todo ha ido bien.
3. Pinta también el __centro de la circunferencia__.

Deberías obtener algo así:

```python
# Número de puntos de la circunferencia que
# vamos a transformar para obtener el perfil
N_perfil =

#se barre un ángulo de 0 a 2 pi

#se crean las coordenadas del los puntos
#de la circunferencia
Xc =
Yc =

#lo visualizamos

```

```python
# Lo visualizamos más bonito
plt.figure("circunferencia", figsize=(5,5))
plt.title('Circunferencia', {'fontsize':20})
# Esto no tienes por qué entenderlo ahora
p = plt.Polygon(list(zip(Xc, Yc)), color="#cccccc", zorder=3)
plt.gca().add_patch(p)

plt.ylim(-1.5, 2)
plt.xlim(-2, 1.5)
plt.grid()
```

### Transformación de cirunferencia a perfil

Ahora estamos en condiciones de __transformar estos puntos de la circunferencia (`Xc`, `Yc`) en los del perfil (`Xp`, `Yp`)__. Para esto vamos a usar nuestra función `transf_yukovski`. Recuerda que esta función recibe y da números complejos. ¿Saldrá un perfil?

```python
# Se transforman los puntos de la circunferencia
Xp, Yp =

# Lo visualizamos

```

```python
# Lo visualizamos más bonito
plt.figure('perfil yukovski', figsize=(10,10))
plt.title('Perfil', {'fontsize':20})

p = plt.Polygon(list(zip(Xp, Yp)), color="#cccccc", zorder=3)
plt.gca().add_patch(p)
plt.gca().set_aspect(1)

plt.xlim(-3, 3)
plt.ylim(-0.4,1)
plt.grid()

```

## 4. Flujo alrededor del cilindro

Para visualizar ahora el flujo alrededor del cilindro recurrimos al __potencial complejo__ de una _corriente uniforme_ que forme un ángulo $\alpha$ con el eje $x$ _en presencia de un cilindro_ (aplicando el teorema del círculo) y se añade un torbellino con la intensidad adecuada para que se cumpla la hipótesis de Kutta en el perfil:

\begin{equation}
f(t)=U_{\infty}\text{·}\left((t-t_{0})\text{·}e^{-i\alpha}+\frac{R^{2}}{t-t_{0}}\text{·}e^{i\alpha}\right)+\frac{i\Gamma}{2\pi}\text{·}ln(t-t_{0})=\Phi+i\Psi
\end{equation}

donde $\Phi$ es el potencial de velocidades y $\Psi$ es la función de corriente.

$$\Gamma = 4  \pi  a  U  (\delta + (1+\lambda)  \alpha)$$

$\Gamma$ es la circulación que hay que añadir al cilindro para que al transformarlo en el perfil se cumpla la condición de Kutta.

Recordando que la función de corriente toma un valor constante en las líneas de corriente, sabemos que: dibujando $\Psi=cte$ se puede visualizar el flujo.

__Pintaremos estas lineas de potencial constante utilizando la función `contour()`, pero antes tendremos que crear una malla circular. Esto será lo primero que hagamos:__

1. Crea un parámetro `N_R` cuyo valor sea el número de puntos que va a tener la malla en dirección radial. Desde otro punto de vista, esta parámetro es el número de círculos concéntricos que forman la malla.
2. Crea dos parámetros `R_min` y `R_max` que representen el radio mínimo y máximo entre los que se extiende la malla. El radio mínimo debe de ser el radio del círculo, porque estamos calculando el aire en el exterior del perfil.
4. La dirección tangencial necesita un sólo parámetro `N_T`, que representa el número de puntos que la malla tendrá en esta dirección. Dicho de otro modo, cuántos puntos forman los círculos concéntricos de la malla.
3. Crea un array `R_` que vaya desde `R_min` hasta `R_max` y que tenga `N_R` elementos. De manera análoga, crea el array `T_`, que al representar los ángulos de los puntos que forman las circunferencias, debe ir de 0 a 2$\pi$, y tener `N_T` elementos.
4. Para trabajar con la malla, deberemos usar coordenadas cartesianas. Crea la malla: `XX, YY` van a ser dos matrices de `N_T · N_R` elementos. Cada elemento de estas matrices se corresponde con un punto de la malla: la matriz `XX` contiene las coordenadas X de cada punto y la matriz `YY`, las coordenadas y.

La manera de generar estas matrices tiene un poco de truco, porque depende de ambos vectores.
Para cada elemento, $x = real(t_0) +  R · cos (T) $ ,  $y = imag(t_0) + R · sin(T)$.

```python
#se crea la malla donde se va pintar la función de corriente

# Dirección radial
N_R =
R_min =
R_max =

# Dirección tangencial
N_T =

R_ =

T_ =

# Crear la malla:
XX =
YY =
```

```python
#pintamos la malla para verla
plt.figure(figsize=(10,10)) #Esto sirve para que se vea grande
plt.???????(XX.flatten(), YY.flatten(), marker='.')
```

__NOTA__: En versiones anteriores se utilizaba una malla rectangular. Esto generaba algunos problemas con los puntos interiores a la hora de pintar las líneas de corriente y los campos de velocidades y presiones. La idea de usar una malla circular está tomada de [este ejercicio](http://nbviewer.ipython.org/github/barbagroup/AeroPython/blob/master/lessons/06_Lesson06_Assignment.ipynb) del curso Aerodynamics-Hydrodynamics with Python de la [Prof. Lorena Barba](http://lorenabarba.com/).

### Probando a transformar la malla

Bueno, lo que queríamos era hacer cosas alrededor de nuestro perfil, ¿no?

Esto lo conseguiremos pintando la función `psi` (tal cual está definida) en los puntos `XX_tau, YY_tau` transformados de los `XX, YY` a través de la función `transf_yukovski`, recuerda que la transformación que tenemos recibe y da números complejos. Como antes, debes separar parte real e imaginaria. En la siguiente celda calcula y transforma tt (donde debería estar almacenada la malla en forma compleja) para obtener `XX_tau, YY_tau`.

```python
tt =

XX_tau, YY_tau =
```

```python
# Comprobamos que los puntos exteriores a la circunferencia se transforman en los puntos exteriores del perfil
#pintamos la malla para verla
plt.figure(figsize=(10,10))
plt.scatter(XX_tau.flatten(), YY_tau.flatten(), marker='.')
```

### Obteniendo el flujo

1. Crea una variable `T` que tenga el valor correspondiente a la circulación $\Gamma$.
2. Utilizando el array `tt`, el valor `T` y los parámetros definidos al principio (`t0, alfa, U...`) crea `f` según la fórmula de arriba (no hace falta que crees una función).
3. Guarda la parte imaginaria de esa función (función de corriente) en una variable `psi`.

```python
# Circulación que hay que añadir al cilindro para
# que se cumpla la hipótesis de Kutta en el perfil
T = 4 * np.pi * a * U * (delta + (1+landa) * alfa)

# Malla compleja
tt =

# Potencial complejo
f = U * ((tt - t0) * np.exp(-alfa * 1j) + R ** 2 / (tt - t0) * np.exp(alfa * 1j) )
f += 1j * T / (2 * np.pi) * np.log(tt - t0)

# Función de corriente
psi =
```

Como la función de corriente toma un valor constante en cada línea de corriente, podemos visualizar el flujo alrededor del cilindro pintando las lineas en las que `psi` toma un valor constante. Para ello utilizaremos la función `contour()` en la malla `XX, YY`. Si no se ve nada prueba a cambiar el número de líneas y los valores máximo y mínimo de la función que se representan.

```python
#lo visualizamos
plt.figure('lineas de corriente', figsize=(10,10))

plt.contour(XX, YY, psi, np.linspace(-5,5,50))
plt.grid()
plt.gca().set_aspect(1)
#plt.xlim(-8, 8)
#plt.ylim(-3, 3)
```

```python
#ponemos el cilindro encima
plt.figure('flujo cilindro', figsize=(10,10))

plt.contour(XX, YY, psi, np.linspace(-5,5,50), colors=['blue', 'blue'])
plt.grid()
plt.gca().set_aspect(1)

p = plt.Polygon(list(zip(Xc, Yc)), color="#cccccc", zorder=3)
plt.gca().add_patch(p)
```

## 5. Flujo alrededor del perfil

```python
plt.figure("flujo perfil", figsize=(12,12))

plt.contour(?????, ?????, psi, np.linspace(-5,5,50))

plt.xlim(-8,8)
plt.ylim(-3,3)
plt.grid()
plt.gca().set_aspect(1)
```

```python
#Ahora ponemos el perfil encima
plt.figure("flujo perfil", figsize=(12,12))

plt.contour(XX_tau, YY_tau, psi, np.linspace(-5, 5, 50), colors=['blue', 'blue'])

p = plt.Polygon(list(zip(Xp, Yp)), color="#cccccc", zorder=10)
plt.gca().add_patch(p)

plt.xlim(-8,8)
plt.ylim(-3,3)
plt.grid()
plt.gca().set_aspect(1)
```

## 6. Interact

__Ahora es un buen momento para jugar con todos los parámetros del problema.
~~¡Prueba a cambiarlos y ejecuta el notebook entero!~~__

__Vamos a usar un `interact`, ¿no?__

Tenemos que crear una función que haga todas las tareas: reciba los argumentos y pinte para llamar a interact con ella. No tenemos más que cortar y pegar.

```python
def transformacion_geometrica(a, landa, delta, N_perfil=100):
    #punto del plano complejo
    t0 =
    #valor del radio de la circunferencia
    R =
    #se barre un ángulo de 0 a 2 pi
    theta =
    #se crean las coordenadas del los puntos
    #de la circunferencia
    Xc =
    Yc =
    #se crean las coordenadas del los puntos
    #del perfil
    Puntos_perfil = transf_yukovski()
    Xp, Yp = np.real(Puntos_perfil) , np.imag(Puntos_perfil)

    #Se pintan la cirunferencia y el perfil
    fig, ax = plt.subplots(1,2)
    fig.set_size_inches(15,15)
    p_c = plt.Polygon(list(zip(Xc, Yc)), color="#cccccc", zorder=1)
    ax[0].add_patch(p_c)
    ax[0].plot(Xc,Yc)
    ax[0].set_aspect(1)
    ax[0].set_xlim(-3, 3)
    ax[0].set_ylim(-2,2)
    ax[0].grid()

    p_p = plt.Polygon(list(zip(Xp, Yp)), color="#cccccc", zorder=1)
    ax[1].add_patch(p_p)
    ax[1].plot(Xp,Yp)
    ax[1].set_aspect(1)

    ax[1].set_xlim(-3, 3)
    ax[1].set_ylim(-2,2)
    ax[1].grid()
```

```python
from IPython.html.widgets import interact
```

```python
w = interact(transformacion_geometrica,
             landa=(-1.,1, 0.01),
             delta=(-1.,1,0.01),
             a=(0,2.,0.1),
             N_perfil=(4, 200) )
```

```python
def flujo_perfil_circunferencia(landa, delta, alfa, U=1,  N_malla = 100):
    N_perfil=100
    a=1
    #punto del plano complejo
    t0 = a * (-landa + delta * 1j)
    #valor del radio de la circunferencia
    R = a * np.sqrt((1 + landa)**2 + delta**2)

    #se barre un ángulo de 0 a 2 pi
    theta = np.linspace(0, 2*np.pi, N_perfil)
    #se crean las coordenadas del los puntos
    #de la circunferencia
    Xc = - a * landa + R * np.cos(theta)
    Yc =   a * delta + R * np.sin(theta)

    #se crean las coordenadas del los puntos
    #del perfil
    Puntos_perfil = transf_yukovski(a, Xc+Yc*1j)
    Xp, Yp = np.real(Puntos_perfil) , np.imag(Puntos_perfil)

    #se crea la malla donde se va pintar la función de corriente

    # Dirección radial
    N_R = 50   # Número de puntos en la dirección radial
    R_min = R
    R_max = 10

    # Dirección tangencial
    N_T = 180   # Número de puntos en la dirección tangencial

    R_ = np.linspace(R_min, R_max, N_R)
    T_ = np.linspace(0, 2*np.pi , N_T)

    # El menos en la XX  es para que el borde de ataque del perfil esté en la izquierda
    XX = - (R_ * np.cos(T_).reshape((-1, 1)) - np.real(t0))
    YY = R_ * np.sin(T_).reshape((-1, 1)) + np.imag(t0)

    tt = XX + YY * 1j

    alfa = np.deg2rad(alfa)
    # Circulación que hay que añadir al cilindro para
    # que se cumpla la hipótesis de Kutta en el perfil
    T = 4 * np.pi * a * U * (delta + (1+landa) * alfa)
    #Potencial complejo
    f = U * ( (tt - t0) * np.exp(-alfa *1j) + R**2 / (tt - t0) * np.exp(alfa * 1j) )
    f += 1j * T / (2* np.pi) * np.log(tt - t0)
    #Función de corriente
    psi = np.imag(f)

    Puntos_plano_tau = transf_yukovski(a, tt)
    XX_tau, YY_tau = np.real(Puntos_plano_tau) , np.imag(Puntos_plano_tau)

    #Se pinta
    fig, ax = plt.subplots(1,2)
    #lineas de corriente
    fig.set_size_inches(15,15)

    ax[0].contour(XX, YY, psi, np.linspace(-10,10,50), colors = ['blue', 'blue'])
    ax[0].grid()
    ax[0].set_aspect(1)
    p = plt.Polygon(list(zip(Xc, Yc)), color="#cccccc", zorder=10)
    ax[0].add_patch(p)
    ax[0].set_xlim(-5, 5)
    ax[0].set_ylim(-2,2)

    ax[1].contour(XX_tau, YY_tau, psi, np.linspace(-10,10,50), colors = ['blue', 'blue'])
    ax[1].grid()
    ax[1].set_aspect(1)
    p = plt.Polygon(list(zip(Xp, Yp)), color="#cccccc", zorder=10)
    ax[1].add_patch(p)
    ax[1].set_xlim(-5, 5)
    ax[1].set_ylim(-2,2)
```

```python
p = interact(flujo_perfil_circunferencia,
             landa=(-1.,1, 0.01),
             delta=(-1.,1,0.01),
             alfa=(0, 30),
             U=(0,10))
```

---

## 7. Pintemos un poco más

Con los datos que ya hemos manejado, sin mucho más esfuerzo, podemos fácilmente pintar la velocidad y la presión del aire alrededor del perfil.

```python
#Velocidad conjugada
dfdt = U * ( 1 * np.exp(-alfa * 1j) - R**2 / (tt - t0)**2 * np.exp(alfa * 1j) )
dfdt += 1j * T / (2*np.pi) * 1 / (tt - t0)
#coeficiente de presion
cp = 1 - np.abs(dfdt)**2 / U**2
```

```python
cmap = plt.cm.RdBu
```

```python
#Se pinta
fig, ax = plt.subplots(1,3)
#lineas de corriente
fig.set_size_inches(15,15)
ax[0].contour(XX, YY, psi, np.linspace(-10,10,50), colors = ['blue', 'blue'])
ax[0].grid()
ax[0].set_aspect(1)
p = plt.Polygon(list(zip(Xc, Yc)), color="#cccccc", zorder=10)
ax[0].add_patch(p)
#Campo de velocidades
ax[1].contourf(XX, YY, np.abs(dfdt), 200, cmap=cmap)
p = plt.Polygon(list(zip(Xc, Yc)), color="#cccccc", zorder=10)
ax[1].set_title('campo de velocidades')
ax[1].add_patch(p)
ax[1].set_aspect(1)
ax[1].grid()
#campo de presiones
ax[2].contourf(XX, YY, cp, 200, cmap=cmap)
p = plt.Polygon(list(zip(Xc, Yc)), color="#cccccc", zorder=10)
ax[2].set_title('coeficiente de presión')
ax[2].add_patch(p)
ax[2].set_aspect(1)
ax[2].grid()
```

```python
#Se pinta
fig, ax = plt.subplots(1,3)
#lineas de corriente
fig.set_size_inches(15,15)
ax[0].contour(XX_tau, YY_tau, psi, np.linspace(-10,10,50), colors = ['blue', 'blue'])
ax[0].grid()
ax[0].set_aspect(1)
p = plt.Polygon(list(zip(Xp, Yp)), color="#cccccc", zorder=10)
ax[0].add_patch(p)
#Campo de velocidades
ax[1].contourf(XX_tau, YY_tau, np.abs(dfdt), 200, cmap=cmap)
p = plt.Polygon(list(zip(Xp, Yp)), color="#cccccc", zorder=10)
ax[1].set_title('campo de velocidades')
ax[1].add_patch(p)
ax[1].set_aspect(1)
ax[1].grid()
#campo de presiones
ax[2].contourf(XX_tau, YY_tau, cp, 200, cmap=cmap)
p = plt.Polygon(list(zip(Xp, Yp)), color="#cccccc", zorder=10)
ax[2].set_title('coeficiente de presión')
ax[2].add_patch(p)
ax[2].set_aspect(1)
ax[2].grid()
```

```python
def cp_perfil_circunferencia(landa, delta, alfa, U=1,  N_malla = 100):
    N_perfil=100
    a=1
    #punto del plano complejo
    t0 = a * (-landa + delta * 1j)
    #valor del radio de la circunferencia
    R = a * np.sqrt((1 + landa)**2 + delta**2)

    #se barre un ángulo de 0 a 2 pi
    theta = np.linspace(0, 2*np.pi, N_perfil)
    #se crean las coordenadas del los puntos
    #de la circunferencia
    Xc = - a * landa + R * np.cos(theta)
    Yc =   a * delta + R * np.sin(theta)

    #se crean las coordenadas del los puntos
    #del perfil
    Puntos_perfil = transf_yukovski(a, Xc+Yc*1j)
    Xp, Yp = np.real(Puntos_perfil) , np.imag(Puntos_perfil)

    #se crea la malla donde se va pintar la función de corriente

    # Dirección radial
    N_R = 50   # Número de puntos en la dirección radial
    R_min = R
    R_max = 10

    # Dirección tangencial
    N_T = 180   # Número de puntos en la dirección tangencial

    R_ = np.linspace(R_min, R_max, N_R)
    T_ = np.linspace(0, 2*np.pi, N_T)

    # El menos en la XX  es para que el borde de ataque del perfil esté en la izquierda
    XX = - (R_ * np.cos(T_).reshape((-1, 1)) - np.real(t0))
    YY = R_ * np.sin(T_).reshape((-1, 1)) + np.imag(t0)

    tt = XX + YY * 1j

    alfa = np.deg2rad(alfa)
    # Circulación que hay que añadir al cilindro para
    # que se cumpla la hipótesis de Kutta en el perfil
    T = 4 * np.pi * a * U * (delta + (1+landa) * alfa)

    #Velocidad conjugada
    dfdt = U * ( 1 * np.exp(-alfa * 1j) - R**2 / (tt - t0)**2 * np.exp(alfa * 1j) )
    dfdt = dfdt + 1j * T / (2*np.pi) * 1 / (tt - t0)
    #coeficiente de presion
    cp = 1 - np.abs(dfdt)**2 / U**2

    Puntos_plano_tau = transf_yukovski(a, tt)
    XX_tau, YY_tau = np.real(Puntos_plano_tau) , np.imag(Puntos_plano_tau)

    #Se pinta
    fig, ax = plt.subplots(1,2)

    #coeficiente de presión
    fig.set_size_inches(15,15)

    ax[0].contourf(XX, YY, cp, 200, cmap=cmap)
    ax[0].grid()
    ax[0].set_aspect(1)
    p = plt.Polygon(list(zip(Xc, Yc)), color="#cccccc", zorder=10)
    ax[0].add_patch(p)
    ax[0].set_xlim(-5, 5)
    ax[0].set_ylim(-3,3)

    ax[1].contourf(XX_tau, YY_tau, cp, 200, cmap=cmap)
    ax[1].grid()
    ax[1].set_aspect(1)
    p = plt.Polygon(list(zip(Xp, Yp)), color="#cccccc", zorder=10)
    ax[1].add_patch(p)
    ax[1].set_xlim(-5, 5)
    ax[1].set_ylim(-3,3)
```

```python
interact(cp_perfil_circunferencia,
         landa=(0.,1, 0.01),
         delta=(0.,1, 0.01),
         alfa=(0, 30),
         U=(0,10))
```

_En esta clase hemos reafirmado nuestros conocimientos de NumPy, matplotlib y Python, general (funciones, bucles, condicionales...) aplicándolos a un ejemplo muy aeronáutico_

Si te ha gustado esta clase:

Tweet

---

####
¡Síguenos en Twitter!

###### Follow @Pybonacci   Follow @Alex__S12   Follow @newlawrence

Curso AeroPython por Juan Luis Cano Rodriguez y Alejandro Sáez Mollejo se distribuye bajo una Licencia Creative Commons Atribución 4.0 Internacional.

---
_Las siguientes celdas contienen configuración del Notebook_

_Para visualizar y utlizar los enlaces a Twitter el notebook debe ejecutarse como [seguro](http://ipython.org/ipython-doc/dev/notebook/security.html)_

    File > Trusted Notebook

```python
%%html
<a href="https://twitter.com/Pybonacci" class="twitter-follow-button" data-show-count="false">Follow @Pybonacci</a>

```

Follow @Pybonacci

```python
# Esta celda da el estilo al notebook
from IPython.core.display import HTML
css_file = '../static/styles/style.css'
HTML(open(css_file, "r").read())
```

/* This template is inspired in the one used by Lorena Barba
in the numerical-mooc repository: https://github.com/numerical-mooc/numerical-mooc
We thank her work and hope you also enjoy the look of the notobooks with this style */

<link href='http://fonts.googleapis.com/css?family=Source+Sans+Pro|Josefin+Sans:400,700,400italic|Ubuntu+Condensed' rel='stylesheet' type='text/css'>

El estilo se ha aplicado =)

```python

```