Physics Research and Education Bootcamp - Computational | Episode 1

License: CC BY-NC-SA  https://creativecommons.org/licenses/by-nc-sa/4.0/

Last Modification: Sept. 3, 2021

# ***Preambulo***

Primero carguemos los paquetes de Python necesarios para trabajar.

```python
import numpy as np
from tqdm import tqdm # Si eres impaciente como yo querras saber cuanto tiempo falta para que un codigo termine de ejecutarse
import matplotlib.pyplot as plt
```

# **Distribuyendo Empanadas con *Montecarlo***

Recordamos el problema de las empanadas que vimos el segundo día. En ese momento, estudiamos distintos métodos para resolver el problema de estimar la probabilidad de que alguien obtenga una empanada repetida al distribuirlas entre un grupo de personas. Entre las soluciones que exploramos estan:

  * Métodos algebráicos
  * Métodos de fuerza bruta
  * Métodos de grafos
  * ?

Hoy estudiaremos como resolver este problema via Montecarlo.

# **Teoría**

Recordemos brevemente que, en el caso de $N$ personas, existen $2N!$ maneras de distribuir 2 empanadas para cada una. Para $N$ pequeño es posible contar cada caso. Para 7 personas ya existen 1,278,945,280 casos. Un approach de fuerza bruta no es práctico en este caso.

Ya que Montecarlo implica la evaluación de un número grande de múltiples escenarios (menor que el total), existe la posibilidad de que muchos casos favorables (sin repeticiones) y desfavorables se nos escapen.

Sin embargo, es posible evaluar varios escenarios de distribución y estimar el cociente entre los casos favorables y desfavorables. Este valor es constante y dependiente del caso de distribución de empanadas que estamos considerando.

Este cociente esta relacionado con la probabilidad de obtener un caso favorable o desfavorable mediante:

\\

$
\begin{align}
p(x=F) + p(x=D) &= 1 \\
\frac{F}{T} + \frac{D}{T} &= 1
\end{align}
$

\\

Equivalentemente:

$
\begin{align}
\frac{F}{D} + 1 &= \frac{T}{D} \\
\frac{1}{\frac{F}{D}+1} &= \frac{D}{T}
\end{align}
$

\\

Recordando, a traves de Montecarlo podemos estimar este cociente a través de:

$\langle \frac{F}{D} \rangle = \frac{1}{M}\sum_{i=1}^{M} \frac{F_{i}}{D_{i}}$

La desviación estándard de este estimador esta dada por:

$ \sigma = \frac{1}{\sqrt{M}}$

\\

Debemos empezar entonces creando un código que nos permita generar escenarios...

# Práctica

```python
def samples(n,k,nsamples):
    """

    Parameters
    ----------
    n : int
        Numero de personas.
    k : int
        Numero de sabores de empanadas.
    nsamples: int
        Numero de samples

    Returns
    -------
    None.

    """
    # El numero total de empanadas, para que le toque 2 a cada uno
    N_empanadas = 2*n

    # Necesitamos calcular un vector que represente las empanadas para luego distribuirlas.
    # Ya que son dos empanadas para cada uno deberia producir lo siguiente:  [0,0,1,1,2,2,3,3,4,4,5,5.......,k,k]
    Empanadas = np.array([i for i in range(k) for j in range(2)])

    # Ahora generamos escenarios en los cuales distribuimos al azar las empanadas representadas por el vector previo.
    # Este proceso lo repetiremos nsamples veces.  Hay muchas maneras de escribir esto
    Samples = [np.random.choice(Empanadas,size = 6, replace=False)\
               for i in range(nsamples)]

```

Ahora necesitamos un código que nos permita verificar si hay o no duplicaciones dentro de cada muestra. Por duplicaciones entedemos los casos en los que dos entradas consecutivas de la muestra tienen el mismo numero asignado:

`sample[0] == sample[1]`

`sample[2] == sample[3]`

`...`

`sample[n-2] == sample[n-1]`

```python
def revisar_duplicaciones(v):
    """

    Parameters
    ----------
    v : Array
        el vector que queremos revisar si hay casos de duplicaciones.

    Returns
    -------
    dup : int
        [0,1] Resultado si hay o no duplicacion.

    """
    dup = 0
    i = 0
    # Nuevamente hay muchas maneras de resolver esto
    while i<len(v)-1 and dup == 0 :
      # Aqui verificamos directamente la condicion, de ocurrir al menos una vez
      # detenemos el calculo. No nos interesa saber cuanto se repite.
        if v[i] == v[i+1]:
            dup = 1
        else:
            i+=2
    return dup
```

Ahora incluimos este código dentro de nuestra función de muestreo:

```python
def samples(n,k,nsamples):
    """

    Parameters
    ----------
    n : int
        Numero de personas.
    k : int
        Numero de sabores de empanadas.
    nsamples: int
        Numero de samples

    Returns
    -------
    Favorables : int
      Numero de casos favorables en la muestra
    Desfavorables : int
      Numero de casos desfavorables en la muestra

    """
    # El numero total de empanadas, para que le toque 2 a cada uno
    N_empanadas = 2*n

    # Necesitamos calcular un vector que represente las empanadas para luego distribuirlas.
    # Ya que son dos empanadas para cada uno deberia producir lo siguiente:  [0,0,1,1,2,2,3,3,4,4,5,5.......,k,k]
    Empanadas = np.array([i for i in range(k) for j in range(2)])

    # Ahora generamos escenarios en los cuales distribuimos al azar las empanadas representadas por el vector previo.
    # Este proceso lo repetiremos nsamples veces.  Hay muchas maneras de escribir esto
    Samples = [np.random.choice(Empanadas,size = 6, replace=False)\
               for i in range(nsamples)]

    # Evaluamos cada muestra para ver si hay duplicaciones o no
    Duplicaciones = sum([revisar_duplicaciones(sample) for sample in Samples])

    # El numero de muestras favorables es la diferencia entre el numero total de muestras y las muestras con duplicaciones
    Favorables = nsamples-Duplicaciones

    # Retornamos el numero de casos favorables y el numero de Duplicaciones
    return Favorables,Duplicaciones
```

Finalmente implementamos el código de Montecarlo. Repetimos el proceso de muestreo un cierto número de iteraciones `niter`, y luego cálculamos la media y la desviación estándard del estimador del cociente:

```python
def Montecarlo(npersonas,nsabores,nsamples,niter):
    """

    Parameters
    ----------
    npersonas : int
        Numero total de personas.
    nsabores : int
        Numero de sabores de empanadas.
    nsamples : int
        Numero de muestras por cada iteracion.
    niter : int
        Numero de iteraciones para estimacion por Montecarlo.

    Returns
    -------
    Tasa_Favorables : float array
        Arreglo (niter x 1) con los valores de la tasa de casos favorables
        para cada iteracion.
    Tasa_Desfavorables : float array
        Arreglo (niter x 1) con los valores de la tasa de casos desfavorables
        para cada iteracion.

    """
    Evals = np.array([samples(npersonas,nsabores,nsamples) for i in tqdm(range(niter))])
    Radio_DF = Evals[:,1]/Evals[:,0]
    Tasa_Favorables = 1/((Radio_DF)+1)
    Tasa_Desfavorables = 1 - Tasa_Favorables
    Tasa_Favorables_media = np.mean(Tasa_Favorables)
    Tasa_Desfavorables_media = np.mean(Tasa_Desfavorables)
    print('\n La tasa de casos favorables media es : {:.4f}'.format(Tasa_Favorables_media))
    print('\n La tasa de casos desfavorables media es : {:.4f}'.format(Tasa_Desfavorables_media))
    return Tasa_Favorables,Tasa_Desfavorables

def Analisis(T_F,T_D):
  T_Fm = np.mean(T_F)
  T_Fs = np.std(T_F)
  T_Dm = np.mean(T_D)
  T_Ds = np.std(T_D)
  print('\n La tasa de casos favorables media es : {:.4f} +/- {:.4f}'.format(T_Fm,T_Fs))
  print('\n La tasa de casos desfavorables media es : {:.4f} +/- {:.4f}'.format(T_Dm,T_Ds))

```

Ahora podemos ejecutar el código y analizar los resultados

```python
Tasa_F,Tasa_D = Montecarlo(3,3,10000,1000)
```

    100%|██████████| 1000/1000 [03:36<00:00,  4.62it/s]

    La tasa de casos favorables media es : 0.5335
    La tasa de casos desfavorables media es : 0.4665

```python
Analisis(Tasa_F,Tasa_D)
```

     La tasa de casos favorables media es : 0.5335 +/- 0.0049

     La tasa de casos desfavorables media es : 0.4665 +/- 0.0049

Recordamos que de acuerdo a la solución teórica:

$\frac{D}{T} = \frac{7}{15} \approx 0.4666...$

$\frac{F}{T} = \frac{8}{15} \approx 0.5333...$

Podemos concluir que hemos podido estimar satisfactoriamente estas probabilidades.

Si ahora hacemos un histograma con los datos podemos observar un estimado de la distribución de los casos favorables y desfavorables

```python
plt.figure()
plt.suptitle('Resultado Montecarlo',fontweight='bold')
plt.hist(Tasa_F,25,label='Tasa Favorable',alpha=0.5)
plt.hist(Tasa_D,25,label='Tasa Desfavorable',alpha=0.5)
plt.ylabel('Frecuencia',fontweight='bold')
plt.xlabel('Fraccion',fontweight='bold')
plt.grid(True)
plt.legend()
```

# Tarea

Generalice el código que hemos desarrollado para $m$ empanadas por persona. Explore ahora qué ocurre al cambiar el número de peronas $N$, la cantidad de sabores por empanadas $k$ y $m$. Reflexione en los resultados.

# Integrando con Montecarlo:

En las preguntas 3 y 4 del problema de ***Los Policias y las Empanadas*** se nos pide calcular el valor esperado de la velocidad $v$ y de la multa $Multa(v)$ dada por una funcion cuadrática.

Aunque estos ejemplos se pueden resolver analíticamente, hoy los usaremos para ilustrar como a través de ***Montecarlo*** podemos calcular integrales.

Repasando los detalles del problema:

* $v \in U[55,85]$
* $Multa(v) = 5(v-70) + (v-70)^{2}$
* $v_{lim} = 70$

Nos piden calcular:

* $E[v \,| \, v>70]$
* $E[Multa(v) \,|\, v  >70]$

Para comenzar, necesitamos un generador de muestras que genere eventos con $v \in U[70,85]$

```python
def generar_velocidades(nsamples,vmin,vmax):
  return np.random.uniform(vmin,vmax+1e-3,nsamples)
```

El siguiente paso es definir la función de multa:

```python
def Multa(v):
  return 5*(v-70) + ((v-70)**2)
```

Finalmente definimos un integrador de Montecarlo:

```python
def Integrador_Montecarlo(x):
  print('El valor esperado es {:.4f} +/- {:.4f}'.format(np.mean(x),np.std(x)))
```

Generarmos `1e7` muestras y observamos los histogramas correspondientes:

```python
velocidades = generar_velocidades(1000000,70,85)

Multas = Multa(velocidades)

plt.figure()
plt.suptitle(r'Histograma de velocidades',fontweight='bold')
plt.hist(velocidades,100)
plt.xlabel('velocidad (km/h)',fontweight='bold')
plt.ylabel('frecuencia',fontweight='bold')
plt.grid()

plt.figure()
plt.suptitle(r'Histograma de Multas',fontweight='bold')
plt.hist(Multas,100)
plt.xlabel('Multa ($)',fontweight='bold')
plt.ylabel('frecuencia',fontweight='bold')
plt.grid()
```

Es evidente cual es el valor esperado de la primera gráfica. De la segunda no es tan obvio.

Ahora simplemente calculamos los valores esperados:

```python

# Calculamos el valor esperado de la velocidad:
Integrador_Montecarlo(velocidades)

# Calculamos el valor esperado de la multa en $:
Integrador_Montecarlo(Multa(velocidades))
```

    El valor esperado es 77.5062 +/- 4.3315
    El valor esperado es 112.6369 +/- 88.2824

Analíticamente podemos calcular (Demuéstrelo usted si aún no lo ha hecho):

* $E[v] = 77.50 \pm 4.33$
* $E[M(v)] = 112.50 \pm 88.21$

# Nota:

La intuición nos puede llevar a resultados erroneos....

Hay que verificar bien y hacer las cuentas.

```python
m_v = np.mean(velocidades)
m_m = np.mean(Multas)

fig = plt.figure(figsize=(10,5))
plt.suptitle(r'Histograma de velocidades',fontweight='bold')
ax1 = fig.add_subplot(1,2,1)
f1 = ax1.hist(velocidades,100)
ax1.plot([m_v, m_v],[0,np.max(f1[0])],label='velocidad media',c='r')
ax1.set_xlabel('velocidad (km/h)',fontweight='bold')
ax1.set_ylabel('frecuencia',fontweight='bold')
ax1.grid()
plt.legend()

ax2 = fig.add_subplot(1,2,2)
f2 = ax2.hist(Multas,100)
ax2.plot([m_m, m_m],[0,np.max(f2[0])],label='Multa media',c='r')
ax2.plot([Multa(m_v),Multa(m_v)],[0,np.max(f2[0])],label='Multa de velocidad media',c='g')
ax2.set_xlabel('multa ($)',fontweight='bold')
ax2.set_ylabel('frecuencia',fontweight='bold')
ax2.grid()
plt.legend()

fig2 = plt.figure()
plt.suptitle('Histograma 2D \n Velocidad Vs. Multa',fontweight='bold')
c = plt.hist2d(velocidades,Multas,20)
plt.scatter(m_v,m_m,label=r'$E[v]\, , \, E[M(v)]$',c='r',s=20)
plt.scatter(m_v,Multa(m_v),label=r'$E[v]\, , \, M(E[v])$',c='blue',s=20)
plt.xlabel('velocidad (km/h)',fontweight='bold')
plt.ylabel('multa ($)',fontweight='bold')
plt.grid()
fig2.colorbar(c[3])
plt.legend()
```

 $E[f(x)] \neq f(E[x])$

\\

Sólo en casos en los que $f$ es lineal existe una igualdad!

Version 1.0, Autor : Antonio Figueroa

```python
#libs summary
import types
def imports():
    for name, val in globals().items():
        if isinstance(val, types.ModuleType):
            yield val.__name__
list(imports())
```

    ['builtins',
     'builtins',
     'IPython.core.shadowns',
     'types',
     'numpy',
     'matplotlib.pyplot']


# Sphere Models

In this section, we describe restricted sphere models, which have been used to represent (tumor)-cells.

Sphere's by their nature have no orientations. Their only parameter is their diameter.

Similarly to Cylinder models, Spheres have different model approximations that make different assumptions on the acquisition scheme. We start with describing the simplest Dot model (S1), and more towards more general models (S4).

# Dot: S1
The Dot model represents a non-diffusing component, which could represent trapped water in glial cells*(Stanisz et al. 1997)*, or axons with a different orientation to the main bundle*(Panagiotaki et al. 2009)*.
Notably, *(Alexander et al. 2010, Veraart et al. 2016)* mentions that the contribution of the Dot model is negligible in \emph{in-vivo} acquisitions.
The signal of a Dot is described by a sphere with a diameter of zero, or equivalently an isotropic Gaussian compartment with $\lambda_{iso}$ set to zero. In other words, it's just a function returning one no matter the input:

\begin{equation}
 E_{\textrm{dot}}=1.
\end{equation}

```python
from dmipy.signal_models import sphere_models
dot = sphere_models.S1Dot()
```

## Soderman Sphere: S2
To keep naming consistency with the cylinder model with the same acquisition assumptions we call this the Soderman Sphere, but the equation is given by *(Balinov et al. 1993)*. The radius $R$ now corresponds to the radius of the sphere.

\begin{equation}
E(q,R|\delta\rightarrow0,\Delta\gg R^2/D)=\left(\frac{3}{(2\pi q R) ^ 2}\left(\frac{\sin(2\pi q R)}{(2\pi q R)} - \cos(2\pi q R)\right)\right) ^2
\end{equation}

```python
import numpy as np
from dmipy.core.acquisition_scheme import acquisition_scheme_from_qvalues
sphere_stejskal_tanner = sphere_models.S2SphereStejskalTannerApproximation()

Nsamples = 100
bvecs = np.tile(np.r_[0., 1., 0.], (Nsamples, 1))  # doesn't matter it has no orientation
qvals = np.linspace(0, 3e5, Nsamples)
delta = 0.01
Delta = 0.03
scheme = acquisition_scheme_from_qvalues(qvals, bvecs, delta, Delta)

import matplotlib.pyplot as plt
%matplotlib inline
for diameter in np.linspace(1e-6, 1e-5, 5):
    plt.plot(qvals, sphere_stejskal_tanner(scheme, diameter=diameter),
             label="Diameter="+str(1e6 * diameter)+"$\mu m$")
plt.legend(fontsize=12)
plt.title("Stejskal-Tanner attenuation over sphere diameter", fontsize=17)
plt.xlabel("q-value [1/m]", fontsize=15)
plt.ylabel("E(q)", fontsize=15);
```

## Callaghan Sphere: S3
*(Callaghan 1995)*. Coming Soon...

## Gaussian Phase Sphere: S4
*(Balinov et al. 1993)* derived the formulation of the Gaussian-Phase approximation for spheres, which models the signal attenuation for finite pulse duration $\delta$ and pulse separation $\Delta$. This approximation has been used for the VERDICT model for tumor characterization *(Panagiotaki et al. 2014)*.

\begin{equation}
\ln\left[E_\perp(G,\delta,\Delta,R)\right]=\frac{2\gamma^2 G^2}{D}\sum_{m=1}^{\infty}\frac{a_m^{-4}}{a^2_mR^2-2}\times\left[2\delta-\dfrac{2 + e^{-a_m^2D(\Delta-\delta)} - 2e^{-a_m^2D\delta} - 2e^{-a_m^2D\Delta}+e^{-a_m^2D(\Delta+\delta)}}{a_m^2D}\right]
\end{equation}

```python
gaussian_phase = sphere_models.S4SphereGaussianPhaseApproximation()
```

## References

- Balinov, Balin, et al. "The NMR self-diffusion method applied to restricted diffusion. Simulation of echo attenuation from molecules in spheres and between planes." Journal of Magnetic Resonance, Series A 104.1 (1993): 17-25.
- Callaghan, Paul T. "Pulsed-gradient spin-echo NMR for planar, cylindrical, and spherical pores under conditions of wall relaxation." Journal of magnetic resonance, Series A 113.1 (1995): 53-59.
- Stanisz, Greg J., et al. "An analytical model of restricted diffusion in bovine optic nerve." Magnetic Resonance in Medicine 37.1 (1997): 103-111.
- Panagiotaki, Eleftheria, et al. "Two-compartment models of the diffusion MR signal in brain white matter." International Conference on Medical Image Computing and Computer-Assisted Intervention. Springer, Berlin, Heidelberg, 2009.
- Alexander, Daniel C., et al. "Orientationally invariant indices of axon diameter and density from diffusion MRI." Neuroimage 52.4 (2010): 1374-1389.
- Veraart, Jelle, Els Fieremans, and Dmitry S. Novikov. "Universal power-law scaling of water diffusion in human brain defines what we see with MRI." arXiv preprint arXiv:1609.09145 (2016).
- Panagiotaki, Eletheria, et al. "Noninvasive quantification of solid tumor microstructure using VERDICT MRI." Cancer research 74.7 (2014): 1902-1912.


# Loaded die: a simple tutorial in choosing priors

A die is loaded so that the expectation of values on its upper face is 4.5. What is the probability distribution?

Example use of the maximum entropy package: the unfair die example from Jaynes, *Probability Theory: The Logic of Science*, 2006.

Suppose you know that the long-run average number on the face of a 6-sided die
tossed many times is 4.5.

What probability $p(x)$ would you assign to rolling $x$ on the next roll?

x	1	2	3	4	5	6

p(x)	?	?	?	?	?	?

Constraints:

$$
\begin{align}
E(f_1(X)) = \sum_{x=1}^6 f_1(x) p(x) &= 4.5, \text{where } f_1(x) = x \\
\text{and} \\
\sum_{x=1}^6 p(x) &= 1
\end{align}
$$

This notebook shows how to use the scikit-maxentropy package to find the probability distribution with maximal information entropy subject to these constraints.

```python
import numpy as np
from maxentropy import FeatureTransformer, MinDivergenceModel
```

```python
samplespace = np.linspace(1, 6, 6, dtype=int)
```

```python
samplespace
```

    array([1, 2, 3, 4, 5, 6])

```python
def f_1(x):
    return x
```

```python
features = [f_1]
```

```python
len(features)
```

    1

```python
model = MinDivergenceModel(features, samplespace)
```

```python
model.params
```

    array([0.])

```python
k = np.array([4.5])
X = np.atleast_2d(k)
```

```python
X
```

    array([[4.5]])

```python
model.fit(X)
```

    MinDivergenceModel(algorithm='CG', features=[<function f_1 at 0x10d40e158>],
                       matrix_format='csr_matrix', prior_log_pdf=None,
                       samplespace=array([1, 2, 3, 4, 5, 6]), vectorized=True,
                       verbose=0)

```python
p = model.probdist()
p
```

    array([0.05435317, 0.07877155, 0.11415998, 0.1654468 , 0.23977444,
           0.34749407])

### Now try minimizing KL divergence from a constant prior. We should get the same model.

```python
log_prior = np.log(np.ones(6)/6)
```

```python
log_prior
```

    array([-1.79175947, -1.79175947, -1.79175947, -1.79175947, -1.79175947,
           -1.79175947])

```python
np.exp(log_prior).sum()
```

    1.0000000000000002

```python
# Another way avoiding overflow:
from scipy.special import logsumexp
np.exp(logsumexp(log_prior))
```

    1.0

```python
@np.vectorize
def prior_log_pdf(x):
    return log_prior[x-1]  # zero-based indexing offset
```

```python
prior_log_pdf(samplespace)
```

    array([-1.79175947, -1.79175947, -1.79175947, -1.79175947, -1.79175947,
           -1.79175947])

```python
type(prior_log_pdf)
```

    numpy.vectorize

```python
model2 = MinDivergenceModel(features, samplespace, prior_log_pdf=prior_log_pdf)
```

```python
model2.fit(X)
```

    MinDivergenceModel(algorithm='CG', features=[<function f_1 at 0x10d40e158>],
                       matrix_format='csr_matrix',
                       prior_log_pdf=<numpy.vectorize object at 0x1a1cd3f630>,
                       samplespace=array([1, 2, 3, 4, 5, 6]), vectorized=True,
                       verbose=0)

```python
p2 = model2.probdist()
p2
```

    array([0.05435317, 0.07877155, 0.11415998, 0.1654468 , 0.23977444,
           0.34749407])

```python
np.allclose(p, p2)
```

    True

```python
%matplotlib inline
import seaborn as sns
import matplotlib.pyplot as plt
```

```python
sns.barplot(np.arange(1, 7), model.probdist())
plt.title('Model 1: Probability $p(x)$ of each die face $x$')
```

### Minimizing KL divergence from a different distribution

Now we demonstrate minimizing KL divergence from a different distribution: $p(1) = p(2) = p(3)=p(4)=p(5)=0.1$ and $p(6) = 0.5$, still subject to the same constraint on the expectation being 4.5

```python
prior2 = np.zeros(6)
```

```python
prior2[:4] = 0.1
prior2[4] = 0.5
prior2[5] = 0.1
```

```python
prior2
```

    array([0.1, 0.1, 0.1, 0.1, 0.5, 0.1])

```python
prior2.sum()
```

    1.0

```python
prior_log_probs = np.log(prior2)
```

```python
prior_log_probs
```

    array([-2.30258509, -2.30258509, -2.30258509, -2.30258509, -0.69314718,
           -2.30258509])

```python
@np.vectorize
def prior_log_pdf(x):
    return prior_log_probs[x-1]  # zero-based indexing offset
```

```python
model3 = MinDivergenceModel(features, samplespace,
                            prior_log_pdf=prior_log_pdf,
                            algorithm='BFGS',
                            verbose=False)
```

#### Before fitting the model, what do we have?

```python
model3.probdist()
```

    array([0.1, 0.1, 0.1, 0.1, 0.5, 0.1])

```python
np.allclose(model.probdist(), model3.probdist())
```

    False

```python
sns.barplot(np.arange(1, 7), model3.probdist())
plt.title('Model 2: Probability $p(x)$ of each die face $x$')
```

Are the constraints satisfied?

```python
model3.expectations()
```

    array([4.1])

No. (We haven't fitted the model yet.)

```python
k
```

    array([4.5])

```python
np.allclose(model3.expectations(), k)
```

    False

#### What is the KL divergence before fitting the model ?

```python
from scipy.stats import entropy
```

```python
np.exp(model3.priorlogprobs)
```

    array([0.1, 0.1, 0.1, 0.1, 0.5, 0.1])

```python
model3.probdist()
```

    array([0.1, 0.1, 0.1, 0.1, 0.5, 0.1])

```python
model3.divergence()
```

    -3.330669073875469e-16

Actually, this is zero, with numerical imprecision. The true KL divergence is always >= 0 by definition.

```python
np.allclose(model3.divergence(), 0)
```

    True

```python
# Verify with scipy.stats.entropy():
D = entropy(model3.probdist(), np.exp(model3.priorlogprobs))
D
```

    2.2204460492503128e-16

```python
np.allclose(model3.divergence(), D)
```

    True

#### Answer: zero.

Before calling model3.fit(X), model3 is identical to the prior.

### Now we fit the model (place constraints on it):

```python
model3.fit(X)
```

    MinDivergenceModel(algorithm='BFGS', features=[<function f_1 at 0x10d40e158>],
                       matrix_format='csr_matrix',
                       prior_log_pdf=<numpy.vectorize object at 0x1a20d3c438>,
                       samplespace=array([1, 2, 3, 4, 5, 6]), vectorized=True,
                       verbose=False)

```python
model3.probdist()
```

    array([0.05125862, 0.06272495, 0.07675624, 0.09392627, 0.57468582,
           0.1406481 ])

```python
sns.barplot(np.arange(1, 7), model3.probdist())
plt.title('Model 2: Probability $p(x)$ of each die face $x$')
```

#### Verify that the expectation (of the sole feature) is 4.5:

```python
model3.expectations()
```

    array([4.5])

```python
np.sum(model3.probdist() * model3.samplespace)
```

    4.5000000000114175


# 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.
import numpy as np
%matplotlib inline
import matplotlib.pyplot as plt
```

## 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."""
    tau = t + a ** 2 / t
    return tau
```

```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 = 100

#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 = np.real(t0) + R * np.cos(theta)
Yc = np.imag(t0) + R * np.sin(theta)

#lo visualizamos
plt.figure("circunferencia", figsize=(5,5))
plt.title('Circunferencia', {'fontsize':20})
plt.scatter(Xc, Yc)
plt.scatter(np.real(t0), np.imag(t0), color='orange', marker='x', s=100)
plt.grid()
```

```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
Puntos_perfil = transf_yukovski(a, Xc+Yc*1j)
Xp, Yp = np.real(Puntos_perfil) , np.imag(Puntos_perfil)

#lo visualizamos
plt.figure("perfil yukovski", figsize=(10,10))
plt.title('Perfil', {'fontsize':20})
plt.scatter(Xp, Yp)
plt.grid()
plt.gca().set_aspect(1)
```

```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)$.

__Recuerda que:__

* Los puntos sobre la circunferencia se transforman en el perfil.
* Los puntos interiores a la circunferencia se transforman en puntos interiores al perfil.
* Los puntos exteriores a la circunferencia se transforman en puntos exteriores al perfil. __Los puntos que nos interesan__.

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

# Crear la malla:
XX =  R_ * np.cos(T_).reshape((-1, 1)) + np.real(t0)
YY = R_ * np.sin(T_).reshape((-1, 1)) + np.imag(t0)
```

```python
#pintamos la malla para verla
plt.figure(figsize=(10,10))
plt.scatter(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$ 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 debe estar almacenada la malla en forma compleja) para obtener `XX_tau, YY_tau`.

Probemos a visualizar como se transforman los puntos de la malla primero.

```python
tt = XX + YY * 1j
tautau = transf_yukovski(a, tt)
XX_tau, YY_tau = np.real(tautau) , np.imag(tautau)
```

```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 = XX + YY * 1j

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

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(XX_tau, YY_tau, 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 = 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 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
```

    :0: FutureWarning: IPython widgets are experimental and may change in the future.

```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=(0.,1), delta=(0.,1), 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), delta=(0.,1), 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 @AeroPython

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/AeroPython" class="twitter-follow-button" data-show-count="false">Follow @AeroPython</a>

```

Follow @AeroPython

```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 =)