# Let's draw samples from a nested Gaussian Process

We will draw samples from a $(m, n) = (2, 1)$ nested Gaussian process following [Zhu and Dunson (2013)](http://amstat.tandfonline.com/doi/abs/10.1080/01621459.2013.838568).

For our first trick, we assume values for the random walk and observation noise variabilities and sample from the state space model

$$\begin{align}
Y_j &\sim Z_j \theta_j + \epsilon_j \\
\theta_{j+1} &= G_j \theta_j + \omega_j \\
\theta &= \begin{bmatrix} U \\ DU \\ A \end{bmatrix} \\
\epsilon_j &\sim \mathcal{N}(0, \sigma^2_\epsilon) \\
\omega_j &\sim \mathcal{N}(0, W_j) \\
Z_j &=
    \begin{pmatrix}
    1 & 0 & 0
    \end{pmatrix} \\
G_j &=
    \begin{pmatrix}
    1 & \delta_j & \frac{\delta_j^2}{2} \\
    0 & 1 & \delta_j \\
    0 & 0 & 1
    \end{pmatrix} \\
W_j & =
    \begin{pmatrix}
    \frac{\delta_j^3}{3} \sigma_U^2 + \frac{\delta_j^5}{20} \sigma_A^2 &
    \frac{\delta_j^2}{2} \sigma_U^2 + \frac{\delta_j^4}{8} \sigma_A^2 &
    \frac{\delta_j^3}{6} \sigma_A^2 \\
    \frac{\delta_j^2}{2} \sigma_U^2 + \frac{\delta_j^4}{8} \sigma_A^2 &
    \delta_j \sigma_U^2 + \frac{\delta_j^3}{3} \sigma_A^2 &
    \frac{\delta_j^2}{2} \sigma_A^2 \\
    \frac{\delta_j^3}{6} \sigma_A^2 &
    \frac{\delta_j^2}{2} \sigma_A^2 &
    \delta_j \sigma_A^2
    \end{pmatrix} \\
\theta_0 &\sim \mathcal{N}(0, \mathrm{diag}(\sigma^2_\mu, \sigma^2_\mu, \sigma^2_\alpha))
\end{align}$$

In the above, we take $\delta_j \equiv t_{j + 1} - t_j$ and assume $\sigma^2_\mu$ and $\sigma^2_\alpha$ are specified as part of the prior.

# Implementation

```python
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import sstools as ss
import ngptools as ngp
%matplotlib inline
sns.set_style('darkgrid')

np.random.seed(12349)  # set random seed
```

## Set up system matrices and dimensions

```python
Nt = 100
tobs = np.linspace(0, 1, Nt)
```

Now we can create the multidimensional arrays needed to sample our state space model:

```python
Np = 1
Nm = 3
Nr = 3

sigeps = 1
sigA = 50
sigU = .25
sigmu = 3
sigalpha = 1.

# we need as many deltas as observations
# the last delta corresponds to a hypothetical point at which we want to make a
# prediction about the state
tpred = 1.1
delta = np.diff(np.concatenate([tobs, [tpred]]))
```

## Create fake data

```python
y, alpha = ngp.generate((Np, Nm, Nr), delta, sigeps, sigU, sigA, sigmu, sigalpha)
```

```python
plt.figure(figsize=(10, 4))
plt.scatter(tobs, y, label='data');
plt.plot(tobs, alpha[:, 0])
plt.xlim(tobs[0], tobs[-1])
pass
```

```python
Ns = 500  # number of samples

alpha_samples = ngp.sample(y, Ns, (Np, Nm, Nr), delta, sigeps, sigU, sigA, sigmu, sigalpha)
```

```python
trans = 5 / Ns
plt.figure(figsize=(10, 8))
th_names = [r'U', r'DU', r'A']

for idx in range(Nm):
    plt.subplot(Nm, 1, idx + 1)
    plt.plot(alpha[..., idx], color=sns.color_palette()[idx], linestyle='--', label='Actual')
    plt.plot(alpha_samples[..., idx].T, color=sns.color_palette()[idx], alpha=trans)
    plt.title(th_names[idx])
    plt.legend()
pass
```

```python

```


```python
import sympy as sym
import numpy
import matplotlib.pyplot as plt
import mpmath

def lagrange_series(x, N):
    psi = []
    #  h = Rational(1, N)
    h = 1.0/N
    points = [i*h for i in range(N+1)]
    for i in range(len(points)):
        p = 1
        for k in range(len(points)):
            if k != i:
                p *= (x - points[k])/(points[i] - points[k])
        psi.append(p)
    psi = psi[1:-1]
    return psi

def analytical():
    eps_values = [1.0, 0.1, 0.01, 0.001]
    for eps in eps_values:
      x = numpy.arange(Omega[0], Omega[1], 1/((N+1)*100.0))
      ue =  (numpy.exp(-x/eps) - 1)/ (numpy.exp(-1/eps) - 1)
      print((len(x), len(ue)))
      plt.plot(x, ue)
    plt.legend(["$\epsilon$=%.1e" % eps for eps in eps_values],
               loc="lower right")
    plt.title("Analytical Solution")
    plt.show()

def bernstein_series(x, N):
    # FIXME: check if a normalization constant is common in the definition
    # advantage is that the basis is always positive
    psi = []
    # for k in range(0,N+1):
    for k in range(1,N):  # bc elsewhere
        psi_k = x**k*(1-x)**(N-k)
        psi.append(psi_k)
    return psi

def sin_series(x, N):
    # FIXME: do not satisfy bc
    psi = []
    for k in range(1,N):
        psi_k = sym.sin(sym.pi*k*x)
        psi.append(psi_k)
    return psi

def series(x, series_type, N):
    if series_type=="sin"  : return sin_series(x, N)
    elif series_type=="Bernstein"  : return bernstein_series(x, N)
    elif series_type=="Lagrange"  : return lagrange_series(x, N)
    else: print("series type unknown ") # sys.exit(0)

def epsilon_experiment(N, series_type, Omega,
                       eps_values = [1.0, 0.1, 0.01, 0.001]):
    # x is global, symbol or array
    psi = series(x, series_type, N)
    f = 1
    for eps in eps_values:
        A = sym.zeros(N-1, N-1)
        b = sym.zeros(N-1)

        for i in range(0, N-1):
            integrand = f*psi[i]
            integrand = sym.lambdify([x], integrand, 'mpmath')
            b[i,0] = mpmath.quad(integrand, [Omega[0], Omega[1]])
            for j in range(0, N-1):
                integrand = eps*sym.diff(psi[i], x)*\
                  sym.diff(psi[j], x) - sym.diff(psi[i], x)*psi[j]
                integrand = sym.lambdify([x], integrand, 'mpmath')
                A[i,j] = mpmath.quad(integrand, [Omega[0], Omega[1]])

        c = A.LUsolve(b)
        u = sum(c[r,0]*psi[r] for r in range(N-1)) + x

        U = sym.lambdify([x], u, modules='numpy')
        x_ = numpy.arange(Omega[0], Omega[1], 1/((N+1)*100.0))
        U_ = U(x_)
        plt.plot(x_, U_)
    plt.legend(["$\epsilon$=%.1e" % eps for eps in eps_values],
               loc="upper left")
    plt.title(series_type)
    plt.show()

if __name__ == '__main__':
    import sys
    if len(sys.argv) > 1:
        series_type = sys.argv[1]
    else:
        series_type = "Bernstein"
    if len(sys.argv) > 2:
        N = int(sys.argv[2])
    else:
    N = 8
    #series_type = "sin"
    #series_type = "Lagrange"

    Omega = [0, 1]
    x = sym.Symbol("x")

    analytical()
    epsilon_experiment(N, series_type, Omega)

```

```python

```

Data Science and Machine Learning/Machine-Learning-In-Python-THOROUGH/EXAMPLES/FINITE_ELEMENTS/INTRO/SRC/05_CONV_DIFF_GLOBAL.ipynb


# Computer Vision  💻 🤓

This notebook contains some implementations of Computer Vision/Image processing algorithms.

### Grayscale resolution

```python
import sys
import math

import numpy as np
import matplotlib.pyplot as plt
from PIL import Image

from __future__ import print_function
from ipywidgets import interact
import ipywidgets as widgets

def threshold(res):
  img = np.asarray(Image.open('images/lena.png').convert('L')).copy()
  step = math.ceil(255/res)
  regions = [(x, x+step, x+step-1) for x in range(0,255,step)]

  for (lo, hi, color) in regions:
    img[np.logical_and(img>=lo, img<hi)] = color

  plt.imshow(img, cmap='gray')
  plt.show()

_ = interact(threshold, res=widgets.IntSlider(min=2,max=16,step=1,value=4))
```

Failed to display Jupyter Widget of type interactive.

  If you're reading this message in the Jupyter Notebook or JupyterLab Notebook, it may mean
  that the widgets JavaScript is still loading. If this message persists, it
  likely means that the widgets JavaScript library is either not installed or
  not enabled. See the Jupyter
  Widgets Documentation for setup instructions.

  If you're reading this message in another frontend (for example, a static
  rendering on GitHub or NBViewer),
  it may mean that your frontend doesn't currently support widgets.

### Variance based thresholding (Otsu, 1979)

Assume image has a grayscale resolution of $L$ gray levels.

The number of pixels with gray level $i$ is written as $n_{i}$, so the total number of pixels in the image is $N = n_{1} + n_{2} + \ldots + n_{L}$. Thus, the probability of a pixel having gray level $i$ is:

\begin{equation}
    p_{i}=\frac{n_{i}}{N}
\end{equation}
where

\begin{equation}
    p_{i}>=0 \qquad \sum_{i=1}^{L} p_{i}=1
\end{equation}

Compute between-class variance $\sigma_{B}^2$ and total variance $\sigma_{T}^2$ using $k$:

\begin{equation}
    \sigma_{B}^2=\pi_{0}(\mu_{0} - \mu_{T})^2 + \pi_{1}(\mu_{1} - \mu_{T})^2
\end{equation}

\begin{equation}
    \sigma_{T}^2=\sum_{i=1}^{L}(i - \mu_{T})^2p_{i}
\end{equation}
where

\begin{equation}
    \pi_{0}=\sum_{i=1}^{k}p_{i} \qquad \pi_{1}=\sum_{i=k+1}^{L}p_{i}=1-{\pi_{0}}
\end{equation}

\begin{equation}
    \mu_{0}=\sum_{i=1}^{k}ip_{i}/\pi_{0} \quad \mu_{1}=\sum_{i=k+1}^{L}ip_{i}/\pi_{1} \quad
    \mu_{T}=\sum_{i=1}^{L}ip_{i}
\end{equation}

The formula for the between-class variance can be simplified to:

\begin{equation}
    \sigma_{B}^2=\pi_{0}\pi_{1}(\mu_{1} - \mu_{0})^2
\end{equation}

For a single threshold, the criterion to be maximized is the ratio of the between-class variance to the total variance:

\begin{equation}
    \eta=\frac{\sigma_{B}^2}{\sigma_{T}^2}
\end{equation}

Taken from Computer and Machine Vision (Fourth Edition): Theory, Algorithms, Practicalities. E.R. Davies.

```python
import sys

import numpy as np
from PIL import Image

image = np.asarray(Image.open('images/lena.png').convert('L'))
img = image.copy()
hist, _ = np.histogram(img, bins=256)

def histogram_variance(hist, k):
    probs = hist / hist.sum()

    pi_zero = np.sum(probs[:k])
    pi_one  = 1 - pi_zero

    def compute_mi_zero():
      factors = np.array(range(0,k)) / pi_zero
      return np.dot(probs[:k], factors)

    def compute_mi_one():
      factors = np.array(range(k+1, probs.size+1)) / pi_one
      return np.dot(probs[k:], factors)

    def compute_mi_t():
      factors = np.array(range(0, probs.size))
      return np.dot(probs, factors)

    mi_zero = compute_mi_zero()
    mi_one  = compute_mi_one()
    mi_t    = compute_mi_t()

    variance_b = pi_zero * pi_one * (mi_one - mi_zero) ** 2

    def compute_variance_t():
      factors = (np.array(range(0, probs.size)) - mi_t) ** 2
      return np.dot(probs, factors)

    variance_t = compute_variance_t()

    variances_ratio = variance_b / variance_t

    return variances_ratio

def variance_based_thresholding(hist):
    ratios = np.array([histogram_variance(hist, x) for x in range(0,256)])
    return np.argmax(ratios), ratios

k, ratios = variance_based_thresholding(hist)

black_region = np.where(img >= k)
white_region = np.where(img < k)

img[black_region] = 0
img[white_region] = 255

fig, (ax1, ax2) = plt.subplots(1, 2)
fig.set_figwidth(10)
ax1.set_title(f'When k={k}')
ax1.imshow(img, cmap='gray')
plt.plot(k, entropies[k], 'go')
ax2.plot(entropies)

ax2.set_title('Variances ratios')
ax2.set_xlabel(f'k')
_ = ax2.set_ylabel('in_between_variance/total_variance')
```

### Entropy based thresholding (Kapur et al., 1985)

Divide intensity probability distribution in two classes: those with gray levels up to the threshold value $k$ and those with gray levels above $k$. This leads to two probability distributions A and B:

\begin{equation}
    A: \frac{p_{1}}{P_{k}}, \frac{p_{2}}{P_{k}}, \ldots, \frac{p_{k}}{P_{k}}
\end{equation}

\begin{equation}
    B: \frac{p_{k+1}}{1-p_{k}}, \frac{p_{k+2}}{1-p_{k}}, \ldots, \frac{p_{L}}{1-p_{k}}
\end{equation}

where:

\begin{equation}
    P_{k}=\sum_{i=1}^{k}p_{i} \qquad 1-p_{k}=\sum_{i=k+1}^{L}p_{i}
\end{equation}

\begin{equation}
    H(A)=-\sum_{i=1}^{k}\frac{p_{i}}{P_{k}}\ln{\frac{p_{i}}{P_{k}}}
\end{equation}

\begin{equation}
    H(B)=-\sum_{i=k+1}^{L}\frac{p_{i}}{1-P_{k}}\ln{\frac{p_{i}}{1-P_{k}}}
\end{equation}

and the total entropy is:

\begin{equation}
    H(k) = H(A) + H(B)
\end{equation}

Taken from Computer and Machine Vision (Fourth Edition): Theory, Algorithms, Practicalities. E.R. Davies.

```python
import sys
import math

import numpy as np
import matplotlib.pyplot as plt
from PIL import Image

image = np.asarray(Image.open('images/lena.png').convert('L'))
img = image.copy()
hist, _ = np.histogram(img, bins=256)

def histogram_entropy(hist, k):
    probs = hist / hist.sum()
    pk = np.sum(probs[:k])
    pk_complement = 1 - pk

    a_dist = probs[:k] / pk
    b_dist = probs[k:] / pk_complement

    def dist_entropy(dist):
        def entropy(x):
            if x > 0:
                return -x * math.log(x)
            else:
                return 0
        entropy_v = np.vectorize(entropy, otypes=[np.dtype('f8')])
        return np.sum(entropy_v(dist))

    entropy_a = dist_entropy(a_dist)
    entropy_b = dist_entropy(b_dist)

    total_entropy = entropy_a + entropy_b

    return total_entropy

def maximum_entropy_thresholding(hist):
    entropies = np.array([histogram_entropy(hist, x) for x in range(0,256)])
    return np.argmax(entropies), entropies

k, entropies = maximum_entropy_thresholding(hist)

black_region = np.where(img >= k)
white_region = np.where(img < k)

img[black_region] = 0
img[white_region] = 255

fig, (ax1, ax2) = plt.subplots(1, 2)
fig.set_figwidth(10)
ax1.set_title(f'When k={k}')
ax1.imshow(img, cmap='gray')
plt.plot(k, entropies[k], 'go')
ax2.plot(entropies)

ax2.set_title('Entropies')
ax2.set_xlabel(f'k')
_ = ax2.set_ylabel('H(k)')
```

### Line Hough transform

```python
import sys
import math

import cv2
import numpy as np
import matplotlib.pyplot as plt

from __future__ import print_function
from ipywidgets import interact
import ipywidgets as widgets

def hough_transform(canny_abajo, canny_arriba, umbral_lineas):
  image_color = cv2.imread("images/cuadrado.png")
  image = cv2.cvtColor(image_color, cv2.COLOR_BGR2GRAY)

  bordes = cv2.Canny(image, canny_abajo, canny_arriba)

  (w,h) = image.shape
  diagonal = math.ceil(math.sqrt(w ** 2 + h** 2))
  espacio_p = np.zeros((diagonal, 181))

  for i in range(w):
    for j in range(h):
      if bordes[i][j] != 0:
        for t in range(-90, 90+1):
          p = (i * math.cos(t)) + (j * math.sin(t))
          espacio_p[math.floor(p)][t+90] += 1

  lineas = np.argwhere(espacio_p > umbral_lineas)

  for (p,theta) in lineas:
    a = math.sin(theta-90)
    b = math.cos(theta-90)
    x = a*p
    y = b*p
    p1 = (math.floor(x+1000*(-b)), math.floor(y+1000*a))
    p2 = (math.floor(x-1000*(-b)), math.floor(y-1000*a))
    cv2.line(image_color, p1, p2, (0,255,0))

  plt.imshow(cv2.cvtColor(image_color, cv2.COLOR_BGR2RGB))
  plt.show()

_ = interact(
    hough_transform,
    canny_abajo=widgets.IntSlider(min=0,max=255,step=1,value=100),
    canny_arriba=widgets.IntSlider(min=0,max=255,step=1,value=200),
    umbral_lineas=widgets.IntSlider(min=0,max=255,step=1,value=70)
)
```

Failed to display Jupyter Widget of type interactive.

  If you're reading this message in the Jupyter Notebook or JupyterLab Notebook, it may mean
  that the widgets JavaScript is still loading. If this message persists, it
  likely means that the widgets JavaScript library is either not installed or
  not enabled. See the Jupyter
  Widgets Documentation for setup instructions.

  If you're reading this message in another frontend (for example, a static
  rendering on GitHub or NBViewer),
  it may mean that your frontend doesn't currently support widgets.

### Circle Hough transform

```python
import sys
import math

import cv2
import numpy as np
import matplotlib.pyplot as plt

from __future__ import print_function
from ipywidgets import interact
import ipywidgets as widgets

def circle_hough_transform(canny_abajo, canny_arriba, umbral_circulos, radio_estimado):
  image_color = cv2.imread("images/circulos.jpg")
  image = cv2.cvtColor(image_color, cv2.COLOR_BGR2GRAY)

  bordes = cv2.Canny(image, canny_abajo, canny_arriba)

  (w, h) = image.shape
  espacio_p = np.zeros((w+1,h+1))

  for i in range(w):
    for j in range(h):
      if bordes[i][j] != 0:
        for t in range(0, 360+1):
          a= i - (radio_estimado*math.cos(t))
          b= j - (radio_estimado*math.sin(t))
          if (a>=0 and b>=0 and a<=w-1 and b<=h-1):
            espacio_p[math.floor(a), math.floor(b)] += 1

  circulo_centros = np.argwhere(espacio_p > umbral_circulos)

  for centro in circulo_centros:
    cv2.circle(image_color, tuple(reversed(centro)), radio_estimado, (0,160,277))

  plt.imshow(cv2.cvtColor(image_color, cv2.COLOR_BGR2RGB))
  plt.show()

_ = interact(
    circle_hough_transform,
    canny_abajo=widgets.IntSlider(min=0, max=255, step=1, value=100),
    canny_arriba=widgets.IntSlider(min=0, max=255, step=1, value=200),
    umbral_circulos=widgets.IntSlider(min=0, max=255, step=1, value=40),
    radio_estimado=widgets.IntSlider(min=0, max=255, step=1, value=100)
)
```

Failed to display Jupyter Widget of type interactive.

  If you're reading this message in the Jupyter Notebook or JupyterLab Notebook, it may mean
  that the widgets JavaScript is still loading. If this message persists, it
  likely means that the widgets JavaScript library is either not installed or
  not enabled. See the Jupyter
  Widgets Documentation for setup instructions.

  If you're reading this message in another frontend (for example, a static
  rendering on GitHub or NBViewer),
  it may mean that your frontend doesn't currently support widgets.

### Custom BFS-based segmentation algorithm

Note that this algorithm is very slow.

```python
import sys
import queue
import random

import cv2
import numpy as np
import matplotlib.pyplot as plt

from __future__ import print_function
from ipywidgets import interact
import ipywidgets as widgets

class Explorador:
  def __init__(self, imagen, d):
    self.imagen = imagen
    self.d = d
    self.vecindario = 1
    self.vecindarios = np.zeros(imagen.shape, dtype=np.int64)
    self.w,self.h = self.imagen.shape
    self.cola = queue.Queue()

  def movimiento_valido(self, m, n):
    return m >= 0 and n >= 0 and m <= self.w - 1 and n <= self.h - 1

  def es_vecino(self, actual, siguiente):
    return siguiente <= actual + self.d and siguiente >= actual - self.d

  def explorado(self, m, n):
    return self.vecindarios[m, n] != 0

  def busqueda(self, m, n):
    self.vecindarios[m,n] = self.vecindario
    self.cola.put((m, n))

    while not self.cola.empty():
      x,y = self.cola.get()
      for direccion_x, direccion_y in [(1, 0),(-1, 0),(0, 1),(0, -1)]:
        movimiento_x = x + direccion_x
        movimiento_y = y + direccion_y
        if self.movimiento_valido(movimiento_x, movimiento_y) and \
           (not self.explorado(movimiento_x, movimiento_y)) and \
           self.es_vecino(self.imagen[x,y], self.imagen[movimiento_x,movimiento_y]):
           self.vecindarios[movimiento_x,movimiento_y] = self.vecindario
           self.cola.put((movimiento_x,movimiento_y))

  def descubrir_vecindarios(self):
    for x in range(self.w):
      for y in range(self.h):
        if self.vecindarios[x,y] == 0: # 0 es no se ha visitado
          self.busqueda(x,y)
          self.vecindario+=1

    return self.vecindarios

def vecindarios_coloreados(imagen):
  color = lambda: (random.randint(0,255),random.randint(0,255),random.randint(0,255))
  w,h = imagen.shape
  coloreada = np.zeros((w,h,3), np.uint8)
  num_vecindarios = np.amax(imagen)
  for vecindario in range(num_vecindarios + 1):
    coloreada[np.where(imagen == (vecindario + 1))] = color()

  return coloreada

def descubrir_vecindarios(d):
  imagen = cv2.imread("images/lena.png", 0)
  e = Explorador(imagen, d)
  resultado = e.descubrir_vecindarios()
  plt.imshow(cv2.cvtColor(vecindarios_coloreados(resultado), cv2.COLOR_BGR2RGB))
  plt.show()

descubrir_vecindarios(3)
```

```python

```


## Geodesics in Heat implementation
This notebook implements Geodesics in Heat [[Crane et al. 2014]](https://arxiv.org/pdf/1204.6216.pdf) for triangle and tet meshes.

Compare to the C++ implementation in `experiments/geodesic_heat/main.cc`.

```python
import sys
sys.path.append('..')
import mesh, differential_operators, sparse_matrices, numpy as np
from tri_mesh_viewer import TriMeshViewer as Viewer
```

```python
volMesh = mesh.Mesh('../../examples/meshes/3D_microstructure.msh', degree=1)
```

```python
# Choose whether to work with the tet mesh or its boundary triangle mesh.
m = volMesh
#m = volMesh.boundaryMesh()
```

```python
# Choose a timestep proportional to h^2 where h is the average edge length.
# (As discussed in section 3.2.4 of the paper)
c = 4 / np.sqrt(3)
t = c *  m.volume / m.numElements()
# Choose source vertex/vertices for computing distances
sourceVertices = [0]
```

We have not yet bound the sparse matrix manipulation and solver functionality of MeshFEM, so we use scipy for now:

```python
import scipy, scipy.sparse, scipy.sparse.linalg
```

Backwards Euler time stepping for heat equation $\frac{\mathrm{d}}{\mathrm{d}t} = \bigtriangleup u, \, \, u|_\gamma = 1 \, \forall t$:
\begin{align}
                         \frac{u_t - u_0}{t} &= \bigtriangleup u_t \\
 \Longrightarrow \quad M \frac{u_t - u_0}{t} &= -L u_t    \quad \text{(positive FEM Laplacian discretizes $-\bigtriangleup$)} \\
 \Longrightarrow \quad \underbrace{(M + t L)}_A u_t &= M u_0
 \end{align}
where $\gamma$ is the domain from which we wish to compute distances (here given by `sourceVertices`)

```python
L = differential_operators.laplacian(m).compressedColumn()
M = differential_operators.mass(m, lumped=False).compressedColumn()
A = L + t * M

mask = np.ones(m.numVertices(), dtype=np.bool)
mask[sourceVertices] = False

A_ff = A[:,  mask][mask, :]
A_fc = A[:, ~mask][mask, :]

# Solve (M + t L) u = 0 with the constraint u[sourceVertices] = 1
u = np.ones(m.numVertices())
u[mask] = scipy.sparse.linalg.spsolve(A_ff, -A_fc @ np.ones(len(sourceVertices)))
```

```python
# Compute the heat gradients
g = differential_operators.gradient(m, u)
# Normalize the gradients to get an approximate gradient of the distance field
X = -g / np.linalg.norm(g, axis=1)[:, np.newaxis]
```

Fit a scalar field's gradients to these normalized gradients $X$ by solving a Poisson equation:

\begin{align}
- \bigtriangleup \phi = -\nabla \cdot X \quad &\text{in } \Omega \\
\frac{\mathrm{d} \phi}{\mathrm{d} {\bf n}} = {\bf n} \cdot X \quad &\text{on } \partial \Omega \\
\phi = 0 \quad &\text{on } \gamma
\end{align}

```python
divX = differential_operators.divergence(m, X)
L_ff = L[:, mask][mask, :]
heatDist = np.zeros(m.numVertices())
heatDist[mask] = scipy.sparse.linalg.spsolve(L_ff, divX[mask])
```

Visualize the approximate distance field.

```python
view = Viewer(m, scalarField=heatDist)
view.show()
```


###### Material desenvolvido para o minicurso: Introdução à solução numérica de EDP's, ministrado no ERMAC/2018 de 5 a 6 de abril de 2018 na Universidade Federal de Lavras, Lavras/MG, Brasil. Autor: [Jonas Laerte Ansoni](http://jonasansoni.blogspot.com.br/).

#  Minicurso: Introdução à solução numérica de EDP's

## 4. Equações elípticas

Já vimos em aulas passadas o método de diferenças finitas para equações diferenciais parciais (EDP's) e o caso envolvendo o problema de convecção e difusão, onde em ambos os casos envolvem a variação temporal das grandezas físicas de interesse. Mas e os casos relacionados com problemas de equílibrio?

### 4.1. Equação de Poisson

Muito problemas na física não tem dependencia do tempo. Matematicamente, esses problemas são representados por equações diferenciais parciais elípticas. Dentre os fenômenos modelados por equações elípticas, podemos citar a distribuição de pressão dentro de um escoamento incompressível, percolação de água sob uma represa e o campo elétrico perto da extremidade de um material condutor. Todos esses casos podem ser modelados pela a __Equação de Poisson__:

\begin{equation}
\label{poisson}
\nabla^2 \phi = f
\tag{1}
\end{equation}

onde $\phi$ (desconhecida) e $f$ (conhecida) são funções do espaço, em um domínio $\Omega$. Para encontrar a solução, exigimos condições de contorno. Estas poderiam ser as condições de contorno de Dirichlet, especificando o valor da solução na fronteira,

\begin{equation}
\phi = b_1 \text{ no } \partial\Omega,
\end{equation}

ou condições de fronteira do tipo de Neumann, especificando a derivada normal da solução na fronteira,

\begin{equation}
\frac{\partial \phi}{\partial n} = b_2 \text{ no } \partial\Omega.
\end{equation}

Um problema de valor de contorno consiste em encontrar $u$, dadas as informações acima. Numericamente, podemos fazer isso usando *métodos de relaxamento* (método de Jacobi, Gauss-Seidel, SOR), que começam com um palpite inicial de $\phi$ e depois iteram para a solução. Vamos descobrir como!

### 4.2. Equação de Laplce

O caso particular em que $f=0$ (homogênio) resulta na equação de Laplace (em coordenadas cartesianas bidimensionais):

\begin{equation}
\label{laplace}
\nabla^2 \phi = \frac{\partial^2 \phi}{\partial x^2}+\frac{\partial^2 \phi}{\partial y^2}= 0,
\tag{2}
\end{equation}
em que $\phi$ é a variável dependente e $\nabla^2$ é o operador laplaciano, em que coordenadas cartesianas bidimencionais, é dado por

\begin{equation}
\nabla^2=\frac{\partial^2}{\partial x^2}+\frac{\partial^2}{\partial y^2}.
\end{equation}

A solução única para esse tipo de problemas é obtida especificando-se condições sobre a variável dependente na fronteira $\partial\Omega$ da região $\Omega$, em que se quer resolver o problema. Problemas que exigem condições ao longo da fronteira (contorno) $\partial\Omega$ de _toda_ a região são denominadas de __problemas de valor de contorno__ (PVC).

#### 4.2.1 Equação de calor em estado estacionário

####  Figure 1. Chapa plana cujas bordas são mantidas a diferentes temperaturas .

Como exemplo considere a chapa plana de metal mostrada na Figura \ref{placa}. Ela está termicamente isolada nas faces, possui espessura desprezível e só pode trocar calor pelas bordas laterais, que são mantidas às temperaturas constantes.

Quando a placa está em equílio térmico, a temperatura $T$ em cada ponto *interno* (isto é, que não esteja na borda) satisfaz a equação \ref{laplace} com $\phi=T$, ou seja:

\begin{equation}
\nabla^2 T = \frac{\partial^2 T}{\partial x^2}+\frac{\partial^2 T}{\partial y^2}=0,
\tag{3}
\end{equation}

onde $x$ e $y$ são funções da pocisão em coordenadas cartesianas.

Aqui precisamos resolver a form 2D da equação de Laplace, satisfazendo as condições de contono representadas na figura \ref{placa}.

O que faremos é encontrar a temperatura no estado estacionário dentro da placa 2D (o que também significa a solução da equação de Laplace), com as condições de contorno indicadas (temperatura da borda da placa). Em seguida, vamos discretizar a região do placa em uma malha computacional, e então discretizamos a equação de Laplace acima com método de diferenças finitas. Esta é a região discretizada da placa.

####  Figure 2. Malha computacional do problema.

#### Discretização em diferenças finitas

Como realizado para a equação da difusão previamente discutida, vamos discretizar os termos derivativos de segunda ordem por meio de diferenças centrais de segunda ordem. Em uma malha cartesiana bidimensional, temos:

\begin{equation}
\frac{T_{i+1, j} - 2T_{i,j} + T_{i-1,j} }{\Delta x^2} + \frac{T_{i,j+1} - 2T_{i,j} + T_{i, j-1} }{\Delta y^2} = 0.
\tag{4}
\end{equation}

A equação anterior pode ser escrita como:

\begin{equation}
T_{i+1,j} - 2T_{i,j} + T_{i-1,j} + \beta^2 (p_{i,j+1} - 2 p_{i,j} + p_{i, j-1} = 0
\tag{5}
\end{equation}

onde $\beta = \frac{\Delta x}{\Delta y}$.

Quando definimos $\Delta x = \Delta y$, obtemos a seguinte equação:

\begin{equation}
T_{i+1, j} + T_{i-1,j} + T_{i,j+1}  + T_{i, j-1}- 4 T_{i,j} = 0
\tag{6}
\label{edf}
\end{equation}

Isso nos diz que o operador diferencial Laplaciano no ponto de grade $(i,j)$ pode ser avaliado discretamente usando o valor de $T$ nesse ponto (com um fator $-4$) e os quatro pontos vizinhos à esquerda, direita, acima e abaixo do ponto da grade $(i, j)$.

### _Pergunta:_  Como é o estêncil do método?

### Resposta:
O estêncil do operador Laplaciano discreto é mostrado na Figura 3. Normalmente é chamado de *estêncil de cinco pontos*, por razões óbvias.

####  Figure 3. Estêncil da equação de diferenças finitas \ref{edf}.

A equação discreta acima (Eq. /ref{edf}) é válida para todos os pontos interiores do domínio. Se escrevermos as equações para *todos* pontos interiores, temos um sistema linear de equações algébricas. A solução deste sistema pode ser obtida por métodos diretos ou iterativos.

Para resolver o problema, usamos um __"chute inicial"__ para os pontos interiores da malha (nós verdes Fig. 4). Inicialmente utilizamos $30^oC$ (ou podemos configurá-lo $35^oC$ ou outro valor), porque não conhecemos o valor dentro da malha (claro, esses são os valores que queremos saber).

No processo iterativo, o valor da temperatura interior da malha irá se ajustando "autocorrigindo", então, quando mais próximo da solução real é o chute inicial, mais rápido obtemos uma solução.

####  Figure 4. Malha computacional com os valores iniciais.

##### Tarefa desafio

Pegue um pedaço de papel e escreva a matriz do coeficiente para uma discretização com 7 pontos de grade na direção $x$ (5 pontos interiores) e 5 pontos na direção $y$ (3 interiores). O sistema deve ter 15 incógnitas e o coeficiente matriz três blocos diagonais. Assuma as condições de contorno prescritas de Dirichlet em todos os lados (não necessariamente zero).

### Vamos ao código!!!

A idéia deste código e de demonstrar de forma didática a implementação do método, você já deve ser capaz de implementar um código mais eficiente.

Se você fez o exercício anterior, observou que a matriz de coeficiente de tal sistema linear tem principalmente zeros. Para uma grade espacial uniforme, a matriz é * diagonal de blocos*. Os métodos iterativos são particularmente adequados para um sistema com essa estrutura e nos poupam de armazenar todos esses zeros.

Vamos começar com um chute inicial para a solução, $T_{i, j}^{0}$, e usar o Laplaciano discreto para obter uma atualização, $ T_{i, j}^{1}$, depois continuar computando $T_{i, j}^{k}$ até ficarmos felizes. Note que $k$ _não_ é um índice de tempo aqui, mas um índice correspondente ao número de iterações que realizamos no *esquema de relaxamento*.

Em cada iteração, calculamos os valores atualizados $T_{i, j}^{k+1}$ de uma maneira (esperançosamente) inteligente para que eles convirjam para um conjunto de valores satisfazendo a equação de Laplace. O sistema atingirá o equilíbrio apenas quando o número de iterações tende a $\infty$, mas podemos aproximar o estado de equilíbrio por iterar até que a mudança entre uma iteração e a próxima seja *muito* pequena.

O método mais intuitivo de solução iterativa é conhecido como o [**método Jacobi**](https://en.wikipedia.org/wiki/Jacobi_method), no qual os valores nos pontos da grade são substituídos pelas médias ponderadas correspondentes:

\begin{equation}
T^{k+1}_{i,j} = \frac{1}{4} \left(T^{k}_{i,j-1} + T^k_{i,j+1} + T^{k}_{i-1,j} + T^k_{i+1,j} \right)
\end{equation}

Lembrando que é para malhas igualmente espaçadas!!!

```python
# Simple Numerical Laplace Equation Solution using Finite Difference Method

import numpy as np
#import matplotlib.pyplot as plt
import matplotlib.pyplot as plt
%matplotlib inline
from matplotlib import rcParams
rcParams['font.family'] = 'serif'
rcParams['font.size'] = 16
```

```python
#Parameters
nx = 41
ny = 41
l2_target = 1e-3
maxIter=1000

L = 10
H = 10

# Boundary condition
Ttop = 100
Tbottom = 0
Tleft = 0
Tright = 0

# Initial guess of interior grid
Tguess = 30

# Set colour interpolation and colour map
colorinterpolation = 50
colourMap = plt.cm.jet #you can try: colourMap = plt.cm.coolwarm
```

O `np.meshgrid()` cria a grade de malha para nós (usamos isso para plotar a solução), o primeiro parâmetro é para a dimensão x, e o segundo parâmetro é para a dimensão y. Usamos np.arange () para arange array 1-D com o valor do elemento começa de algum valor para algum valor, no nosso caso é de `0` para `lenX` e de `0` para `lenY`. Em seguida, definimos a região: definimos a matriz 2-D, definimos o tamanho e preenchemos a matriz com valor de palpite, depois definimos a condição de contorno, observamos a sintaxe de preenchimento do elemento da matriz para a condição de limite acima aqui.

##### Linhas, colunas e ordem do índice

Lembre-se que na aula ?? armazenamos dados com o $y$ coordenadas correspondentes às linhas da matriz e coordenadas $x$ nas colunas (isso é apenas uma decisão de design de código!). Fizemos isso para que uma plotagem dos valores da matriz 2D tivesse a ordenação natural, correspondente ao domínio físico (coordenada $y$ na vertical).

Nós seguiremos a mesma convenção aqui (mesmo que consigamos plotar em 3D, então não há razão real), apenas para ser consistente. Assim, $T_{i, j}$ será armazenado em formato de array como `T [j, i]`. Não fique confuso com isso.

```python
dx = L/(nx-1)
dy = H/(ny-1)

# Mesh
x  = np.linspace(0,L,nx)
y  = np.linspace(0,H,ny)
X,Y = np.meshgrid(x,y)

# Set array size and set the interior value with Tguess
T = np.empty((nx, ny))
T.fill(Tguess)
```

Definindo as condições de contorno do tipo Dirichlet.

```python
# Set Boundary condition
T[(ny-1):, :] = Ttop
T[:1, :] = Tbottom
T[:, (nx-1):] = Tright
T[:, :1] = Tleft

l2norm = 1
iterations = 0
Tn = np.ones((nx,ny))
l2_conv = []
```

### Quanto tempo nós iteramos?

Observamos acima que não há dependência de tempo na equação de Laplace. Portanto, não faz muito sentido usar um loop `for` com iterações` nt`, como fizemos anteriormente.

Em vez disso, podemos usar um loop `while` que continua a aplicar iterativamente o esquema de relaxação até que a diferença entre duas iterações sucessivas seja pequena o suficiente.

Mas quão pequeno é pequeno o suficiente? Esta é uma boa pergunta. Vamos tentar resolver isso enquanto avançamos.

Para comparar dois campos sucessivos de temperatura, podemos usar por exemplo a [norma L2][1] da diferença. É definido como

\begin{equation}
|\textbf{x}| = \sqrt{\sum_{i=0, j=0}^k \left|T^{k+1}_{i,j} - T^k_{i,j}\right|^2}
\end{equation}

Mas há uma falha com esta fórmula. Estamos somando a diferença entre iterações sucessivas em cada ponto da grade. Então, o que acontece quando a grade cresce? (Por exemplo, se estivermos refinando a grade, por qualquer motivo.) Haverá mais pontos de grade para comparar e mais contribuições para a soma. A norma será um número maior só por causa do tamanho da grade!

Isso não parece certo. Vamos corrigi-lo normalizando a norma, dividindo a fórmula acima pela norma do campo potencial na iteração $k$.

Para duas iterações sucessivas, a norma L2 relativa é então calculada como

\begin{equation}
|\textbf{x}|_{L_2} = \frac{\sqrt{\sum_{i=0, j=0}^k \left|T^{k+1}_{i,j} - T^k_{i,j}\right|^2}}{\sqrt{\sum_{i=0, j=0}^k \left|T^k_{i,j}\right|^2}}
\end{equation}

Nosso código Python para este cálculo é uma função de uma linha:

[1]:http://en.wikipedia.org/wiki/Norm_%28mathematics%29#Euclidean_norm

```python
def L2_error(T, Tn):
    return np.sqrt(np.sum((T - Tn)**2)/np.sum(T**2))
```

Norma $L_{\infty}$

\begin{equation}
|\textbf{x}|_{L_{\infty}} = \frac{\sum_{i=0, j=0}^k \left|T^{k+1}_{i,j} - T^k_{i,j}\right|}{\sum_{i=0, j=0}^k \left|T^{k+1}_{i,j}\right|}
\end{equation}

```python
def norm_inf(T, Tn):
    return np.sum((T - Tn)**2)/np.sum(T**2)
```

Agora, vamos definir uma função que irá aplicar o método de Jacobi para a equação de Laplace.

```python
# Iteration
print("Please wait for a moment")

while (l2norm>1e-4):
    Tn=T.copy()
    #T[1:-1,1:-1] = .25 * (Tn[1:-1,2:] + Tn[1:-1,:-2] + Tn[2:,1:-1] + Tn[:-2,1:-1]) #substituir pelos for's de i,j

    for i in range(1,nx-1):
        for j in range(1,ny-1):
            T[i, j] = 0.25 * (Tn[i+1][j] + Tn[i-1][j] + Tn[i][j+1] + Tn[i][j-1])

    l2norm = L2_error(T, Tn)
    #l2norm = np.sqrt(np.sum((Tn - T)**2)/np.sum(Tn**2))
    iterations += 1
    l2_conv.append(l2norm)

    #print("Iterations %d is %.8f  " % (iterations, l2norm))

    if (iterations>=maxIter):
        break

print("Iteration finished")
print ("Jacobi method took {} iterations at tolerance {}".\
        format(iterations, l2norm))

plt.figure(figsize=(8,5))
# Configure the contour
plt.title("Contour of Temperature")
plt.contourf(X, Y, T, colorinterpolation, cmap=colourMap)

# Set Colorbar
plt.colorbar()

# Show the result in the plot window
plt.show()

print("")
```

### Convergência algébrica
Lembre-se que salvamos a norma L2 da diferença entre duas iterações consecutivas. O objetivo disso foi observar como o esquema de relaxamento *converge*, no sentido algébrico: com soluções consecutivas aproximando-se cada vez mais umas das outras. Vamos usar um gráfico de linhas para isso.

```python
plt.figure(figsize=(8,6))
plt.grid(True)
plt.xlabel(r'iterations', fontsize=18)
plt.ylabel(r'$L_2$-norm', fontsize=18)
plt.semilogy(np.arange(len(l2_conv)), l2_conv, lw=2, color='k');
```

Parece que no início, as iterações começaram a convergir rapidamente, mas rapidamente adotaram uma taxa mais lenta. Como vimos antes, foram necessárias mais de ??? iterações para chegar à nossa meta de diferença entre duas soluções consecutivas (na norma L2). Isso é muita iterações, e nós realmente gostaríamos de relaxar mais rápido! Não se preocupe, vamos aprender a fazer isso no próximo notebook.

```python
from IPython.core.display import HTML
css_file = '../styles/custom.css'
HTML(open(css_file, "r").read())
```


```python
from mpl_toolkits.mplot3d import axes3d
import sympy
import pandas as pd
from sympy import *
import matplotlib
from sympy.plotting import plot as symplot
from sympy.plotting import plot3d
from matplotlib.colors import BoundaryNorm
from matplotlib.ticker import MaxNLocator
import matplotlib.pyplot as plt
import numpy as np
from pylab import meshgrid,cm,imshow,contour,clabel,colorbar,axis,title,show
from sympy.vector import CoordSys3D, divergence, Del, dot
```

```python
x = symbols('x')
y = symbols('y')
z = symbols('z')
r = symbols('r')
R = symbols('R')
```

```python
w = (3*(1-r/R)**2)
print('Velocity equation in k direction')
display(w)
print('density equation in terms of r')
rho = 100*(1-0.5*r/R)
display(rho)
print('velocity times density')
expand(w*rho)
```

    Velocity equation in k direction

$\displaystyle 3 \left(1 - \frac{r}{R}\right)^{2}$

    density equation in terms of r

$\displaystyle 100 - \frac{50.0 r}{R}$

    velocity times density

$\displaystyle 300 - \frac{750.0 r}{R} + \frac{600.0 r^{2}}{R^{2}} - \frac{150.0 r^{3}}{R^{3}}$

```python
mean = (w*r*pi/2)/(1/4*pi*R**2)
print('equation to integrate to get mean velocity')
display(mean)
mean_vel = integrate(mean,(r,0,R))
print('mean velocity:')
display(mean_vel)
```

    equation to integrate to get mean velocity

$\displaystyle \frac{6.0 r \left(1 - \frac{r}{R}\right)^{2}}{R^{2}}$

    mean velocity:

$\displaystyle 0.5$

```python
mass = (rho*w*r*pi/2)
print('funcion a integrar pi/2 * rho * w * r')
display(mass)
mass_flow = integrate(mass,(r,0,R))
print('mass flow:')
display(mass_flow)
```

    funcion a integrar pi/2 * rho * w * r

$\displaystyle \frac{3 \pi r \left(1 - \frac{r}{R}\right)^{2} \left(100 - \frac{50.0 r}{R}\right)}{2}$

    mass flow:

$\displaystyle 10.0 \pi R^{2}$

## Parte de volumenes finitos para estimar mass flow
$ \dot {m} = \frac{\Pi}{2} \int \rho \textbf{v} r dr \therefore m = \frac{\Pi}{2} (\rho \textbf{v}  r) \Delta r $
## Para estimar mean velocity
$ w = 2wr \Delta r$

```python
mean = mean.subs(R,1)
```

```python
mass = mass.subs(R,1)
```

```python
dr = 0.01
R = 1
nodos = int(R/dr)
```

```python
rad = np.linspace(start = 0, stop = 1, num=nodos+1, endpoint=True)
```

```python
integral_mean = np.zeros(len(rad))
integral_mass = np.zeros(len(rad))
for i in range(len(rad)-1):
    integral_mean[i] = (mean.subs(r,rad[i]) + mean.subs(r,rad[i+1]))/2*dr
```

```python
print('velocidad promedio')
sum(integral_mean)
```

    velocidad promedio

    0.4999500000000001

```python
for i in range(len(rad)-1):
    integral_mass[i] = (mass.subs(r,rad[i]) + mass.subs(r,rad[i+1]))/2*dr
```

```python
print('flujo másico')
sum(integral_mass)
```

    flujo másico

    31.411999623620755

# Problem 2

```python
f = Function('f')(x,y)
fxx = f.diff(x,x)
fy = f.diff(y)
eq = Eq(fxx + fy + f)

display(eq)
c = CoordSys3D('c', transformation='cylindrical', variable_names=("r", "theta", "z"))
```

$\displaystyle f{\left(x,y \right)} + \frac{\partial^{2}}{\partial x^{2}} f{\left(x,y \right)} + \frac{\partial}{\partial y} f{\left(x,y \right)} = 0$

## Boundary Conditions:
$Zp - Zw = 0$

$Zp - Zs = 0$

$Z(x=0) = 1$

$Z(y=0) = 1$

### Backward Difference Scheme on the dz/dy
$\frac{1}{\Delta x^2} (Zw+Ze) - Zp (1-\frac{2}{\Delta x^2}+\frac{1}{\Delta y}) - \frac{Zs}{\Delta y} = 0$

```python
L = 1
delta = 0.1
dx = delta
dy = delta
w = 1/(dx**2)
e = 1/(dx**2)
s = -1/(dy)
alpha = 1-2/(dx**2) + 1/dy
nodos = int(L/delta)
x = np.linspace(start = 0, stop = L, num=nodos+1, endpoint=True)
Nx = int(len(x))
Ny = int(len(x))
```

```python
res = np.zeros((len(x),len(x)))
p = np.zeros(len(x)**2)
m = np.zeros((len(x)**2,len(x)**2))
# dZ/dy = 0
for i in range(0,Nx):
    m[i,i] = 1
    m[i,i+Nx] = -1
    p[i]=0
    #print(i,1,i+Nx)
step = i

# Z = 1 at x = 0
for i in range(step+1,step+Ny-1):
    m[i,(i-step)*(Nx)] = 1
    #print(i,2, (i-step)*(Nx))
    p[i] = 1
step = i

# dZ/dx = 0
for i in range(step+1, step+Ny-1):
    m[i,(i-step-1)*Nx+(Nx*2-1)] = 1
    m[i,(i-step-1)*Nx+(Nx*2-2)] = -1
    p[i] = 0
    #print(i,3,(i-step-1)*Nx+(Nx*2-2))
step = i

# Z = 1 at y= 0
for i in range(step+1, step+1+ Nx):
    m[i,(i-step-1)+Nx*Ny-Ny] = 1
    p[i] = 1
    #print(i,4,(i-step-1)+Nx*Ny-Ny)
step = i

# Inside
for i in range(0, (Nx-2)):
        #m[i,j*]
        for j in range(0,Ny-2):
            step = step+1
            # Zp
            m[step,i*(Nx)+(j+Nx+1)] = alpha
            #ZW
            m[step,i*(Nx)+(j+Nx+1)-1] = w
            #ZE
            m[step,i*(Nx)+(j+Nx+1)+1] = e
            #ZS
            m[step,i*(Nx)+(j+Nx+1)+Nx] = s
            p[step] = 0
            #print(step,5,i*(Nx)+(j+Nx+1)+Nx)
```

```python
m_inv = np.linalg.inv(m)
Z = p
resultado = np.dot(m_inv,Z)
resultado = resultado.reshape((Nx,Nx))
plt.imshow(resultado, cmap = 'jet', extent=[0,1, 0,1])
plt.title('2D Finite Volume ')
plt.colorbar()
plt.show()
```

```python
# A deltas bajos BDS muestra problemas... se modifica el metodo
```

```python
L = 1
delta = 0.01
dx = delta
dy = delta
w = 1/(dx**2)
e = 1/(dx**2)
n = 1/(2*dy)
s = -1/(2*dy)
alpha = 1-2/(dx**2)
nodos = int(L/delta)
x = np.linspace(start = 0, stop = L, num=nodos+1, endpoint=True)
Nx = int(len(x))
Ny = int(len(x))
```

### Central Difference Scheme on dz/dy
$\frac{1}{\Delta x^2} (Zw+Ze) - Zp (1-\frac{2}{\Delta x^2}) + \frac{Zn}{2\Delta y} - \frac{Zs}{2\Delta y} = 0$

```python
res = np.zeros((len(x),len(x)))
p = np.zeros(len(x)**2)
m = np.zeros((len(x)**2,len(x)**2))
# dZ/dy = 0
for i in range(0,Nx):
    m[i,i] = 1
    m[i,i+Nx] = -1
    p[i]=0
    #print(i,1,i+Nx)
step = i

# Z = 1 at x = 0
for i in range(step+1,step+Ny-1):
    m[i,(i-step)*(Nx)] = 1
    #print(i,2, (i-step)*(Nx))
    p[i] = 1
step = i

# dZ/dx = 0
for i in range(step+1, step+Ny-1):
    m[i,(i-step-1)*Nx+(Nx*2-1)] = 1
    m[i,(i-step-1)*Nx+(Nx*2-2)] = -1
    p[i] = 0
    #print(i,3,(i-step-1)*Nx+(Nx*2-2))
step = i

# Z = 1 at y= 0
for i in range(step+1, step+1+ Nx):
    m[i,(i-step-1)+Nx*Ny-Ny] = 1
    p[i] = 1
    #print(i,4,(i-step-1)+Nx*Ny-Ny)
step = i

# Inside
for i in range(0, (Nx-2)):
        #m[i,j*]
        for j in range(0,Ny-2):
            step = step+1
            # Zp
            m[step,i*(Nx)+(j+Nx+1)] = alpha
            #ZW
            m[step,i*(Nx)+(j+Nx+1)-1] = w
            #ZE
            m[step,i*(Nx)+(j+Nx+1)+1] = e
            #ZN
            m[step,i*(Nx)+(j+Nx+1)-Nx] = n
            #ZS
            m[step,i*(Nx)+(j+Nx+1)+Nx] = s
            p[step] = 0
            #print(step,5,i*(Nx)+(j+Nx+1)-Nx)
```

```python
m_inv = np.linalg.inv(m)
Z = p
resultado = np.dot(m_inv,Z)
resultado = resultado.reshape((Nx,Nx))
plt.imshow(resultado, cmap = 'jet', extent=[0,1, 0,1])
plt.title('2D Finite Volume ')
plt.colorbar()
plt.show()
```

```python

```

```python

```


```python
# import libraries and modules
import numpy as np
import sympy as sp
from scipy.integrate import odeint
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from matplotlib.animation import PillowWriter
```

```python
# The symbols and the derivatives which will be used for later calculations

x1, y1, x2, y2 = sp.symbols(r'x_1 y_1 x_2 y_2',real = True)

m1, m2, g , t= sp.symbols(r'm_1 m_2 g t', real = True, positive = True)

th1 = sp.Function(r'\theta_1')(t)
th2 = sp.Function(r'\theta_2')(t)

th1_d = sp.diff(th1, t)
th2_d = sp.diff(th2, t)
th1_dd = sp.diff(th1_d, t)
th2_dd = sp.diff(th2_d, t)

# the lengths of the 2 pendulums
l1 = 2
l2 = 1
```

```python
# The relationship of x and y with the angle of the pendulum
x1 = l1*sp.sin(th1)
y1 = -l1*sp.cos(th1)
x2 = l2*sp.sin(th2) + x1
y2 = -l2*sp.cos(th2) + y1

x1_d = sp.diff(x1, t)
y1_d = sp.diff(y1, t)
x2_d = sp.diff(x2, t)
y2_d = sp.diff(y2, t)
```

```python
# the x and y functions which will be later used to get the values of the x and y coordinates of the 2 pendulums.
x1_f = sp.lambdify(th1, x1)
y1_f = sp.lambdify(th1, y1)
x2_f = sp.lambdify((th1,th2), x2)
y2_f = sp.lambdify((th1, th2), y2)
```

```python
# Kinetic enegy
T1 = 1/2 * m1 * (x1_d**2 + y1_d**2)
T2 = 1/2 * m2 * (x2_d**2 + y2_d**2)
T = T1+T2
# Potential energy
V1 = m1*g*y1
V2 = m2*g*y2
V = V1 + V2
# Lagrangian
L = T-V
L
```

$\displaystyle 2 g m_{1} \cos{\left(\theta_{1}{\left(t \right)} \right)} - g m_{2} \left(- 2 \cos{\left(\theta_{1}{\left(t \right)} \right)} - \cos{\left(\theta_{2}{\left(t \right)} \right)}\right) + 0.5 m_{1} \left(4 \sin^{2}{\left(\theta_{1}{\left(t \right)} \right)} \left(\frac{d}{d t} \theta_{1}{\left(t \right)}\right)^{2} + 4 \cos^{2}{\left(\theta_{1}{\left(t \right)} \right)} \left(\frac{d}{d t} \theta_{1}{\left(t \right)}\right)^{2}\right) + 0.5 m_{2} \left(\left(2 \sin{\left(\theta_{1}{\left(t \right)} \right)} \frac{d}{d t} \theta_{1}{\left(t \right)} + \sin{\left(\theta_{2}{\left(t \right)} \right)} \frac{d}{d t} \theta_{2}{\left(t \right)}\right)^{2} + \left(2 \cos{\left(\theta_{1}{\left(t \right)} \right)} \frac{d}{d t} \theta_{1}{\left(t \right)} + \cos{\left(\theta_{2}{\left(t \right)} \right)} \frac{d}{d t} \theta_{2}{\left(t \right)}\right)^{2}\right)$

```python
# The lagranfe's equations
LE1 = sp.diff(sp.diff(L, th1_d), t).simplify() - sp.diff(L, th1)
LE2 = sp.diff(sp.diff(L, th2_d), t).simplify() - sp.diff(L, th2)
```

```python
# solving the lagrange's equationn for the second derivatives of theta1 and theta2
sols = sp.solve([LE1, LE2], (th1_dd, th2_dd))
sols[th1_dd]
```

$\displaystyle \frac{g m_{1} \sin{\left(\theta_{1}{\left(t \right)} \right)}}{- 2.0 m_{1} + 2.0 m_{2} \cos^{2}{\left(\theta_{1}{\left(t \right)} - \theta_{2}{\left(t \right)} \right)} - 2.0 m_{2}} + \frac{g m_{2} \sin{\left(\theta_{1}{\left(t \right)} \right)}}{- 2.0 m_{1} + 2.0 m_{2} \cos^{2}{\left(\theta_{1}{\left(t \right)} - \theta_{2}{\left(t \right)} \right)} - 2.0 m_{2}} - \frac{g m_{2} \sin{\left(\theta_{2}{\left(t \right)} \right)} \cos{\left(\theta_{1}{\left(t \right)} - \theta_{2}{\left(t \right)} \right)}}{- 2.0 m_{1} + 2.0 m_{2} \cos^{2}{\left(\theta_{1}{\left(t \right)} - \theta_{2}{\left(t \right)} \right)} - 2.0 m_{2}} + \frac{2.0 m_{2} \sin{\left(\theta_{1}{\left(t \right)} - \theta_{2}{\left(t \right)} \right)} \cos{\left(\theta_{1}{\left(t \right)} - \theta_{2}{\left(t \right)} \right)} \left(\frac{d}{d t} \theta_{1}{\left(t \right)}\right)^{2}}{- 2.0 m_{1} + 2.0 m_{2} \cos^{2}{\left(\theta_{1}{\left(t \right)} - \theta_{2}{\left(t \right)} \right)} - 2.0 m_{2}} - \frac{2.0 m_{2} \sin{\left(\theta_{1}{\left(t \right)} - \theta_{2}{\left(t \right)} \right)} \cos{\left(\theta_{1}{\left(t \right)} - \theta_{2}{\left(t \right)} \right)} \frac{d}{d t} \theta_{1}{\left(t \right)} \frac{d}{d t} \theta_{2}{\left(t \right)}}{- 2.0 m_{1} + 2.0 m_{2} \cos^{2}{\left(\theta_{1}{\left(t \right)} - \theta_{2}{\left(t \right)} \right)} - 2.0 m_{2}} - \frac{m_{2} \sin{\left(\theta_{1}{\left(t \right)} - \theta_{2}{\left(t \right)} \right)} \frac{d}{d t} \theta_{1}{\left(t \right)} \frac{d}{d t} \theta_{2}{\left(t \right)}}{- 2.0 m_{1} + 2.0 m_{2} \cos^{2}{\left(\theta_{1}{\left(t \right)} - \theta_{2}{\left(t \right)} \right)} - 2.0 m_{2}} + \frac{m_{2} \sin{\left(\theta_{1}{\left(t \right)} - \theta_{2}{\left(t \right)} \right)} \left(\frac{d}{d t} \theta_{2}{\left(t \right)}\right)^{2}}{- 2.0 m_{1} + 2.0 m_{2} \cos^{2}{\left(\theta_{1}{\left(t \right)} - \theta_{2}{\left(t \right)} \right)} - 2.0 m_{2}} + \frac{2.0 m_{2} \sin{\left(\theta_{1}{\left(t \right)} \right)} \cos{\left(\theta_{1}{\left(t \right)} - \theta_{2}{\left(t \right)} \right)} \cos{\left(\theta_{2}{\left(t \right)} \right)} \frac{d}{d t} \theta_{1}{\left(t \right)} \frac{d}{d t} \theta_{2}{\left(t \right)}}{- 2.0 m_{1} + 2.0 m_{2} \cos^{2}{\left(\theta_{1}{\left(t \right)} - \theta_{2}{\left(t \right)} \right)} - 2.0 m_{2}} + \frac{m_{2} \sin{\left(\theta_{1}{\left(t \right)} \right)} \cos{\left(\theta_{2}{\left(t \right)} \right)} \frac{d}{d t} \theta_{1}{\left(t \right)} \frac{d}{d t} \theta_{2}{\left(t \right)}}{- 2.0 m_{1} + 2.0 m_{2} \cos^{2}{\left(\theta_{1}{\left(t \right)} - \theta_{2}{\left(t \right)} \right)} - 2.0 m_{2}} - \frac{2.0 m_{2} \sin{\left(\theta_{2}{\left(t \right)} \right)} \cos{\left(\theta_{1}{\left(t \right)} - \theta_{2}{\left(t \right)} \right)} \cos{\left(\theta_{1}{\left(t \right)} \right)} \frac{d}{d t} \theta_{1}{\left(t \right)} \frac{d}{d t} \theta_{2}{\left(t \right)}}{- 2.0 m_{1} + 2.0 m_{2} \cos^{2}{\left(\theta_{1}{\left(t \right)} - \theta_{2}{\left(t \right)} \right)} - 2.0 m_{2}} - \frac{m_{2} \sin{\left(\theta_{2}{\left(t \right)} \right)} \cos{\left(\theta_{1}{\left(t \right)} \right)} \frac{d}{d t} \theta_{1}{\left(t \right)} \frac{d}{d t} \theta_{2}{\left(t \right)}}{- 2.0 m_{1} + 2.0 m_{2} \cos^{2}{\left(\theta_{1}{\left(t \right)} - \theta_{2}{\left(t \right)} \right)} - 2.0 m_{2}}$

```python
# Converting the second derivatives to actual numeric conditions which can be used to actually solve the differential equations.
deriv2_th2_f = sp.lambdify([g,m1, m2, th1, th2, th1_d, th2_d], sols[th2_dd])
deriv2_th1_f = sp.lambdify([g, m1, m2, th1, th2, th1_d, th2_d], sols[th1_dd])

deriv1_th1_f = sp.lambdify(th1_d, th1_d)
deriv1_th2_f = sp.lambdify(th2_d, th2_d)
```

# Equations to solve in this system are:

-   $\frac{d\theta_1}{dt} = v_1$
-     $\frac{dv_1}{dt} = sols[th1\_dd]$
-     $\frac{d\theta_2}{dt} = v_2$
-     $\frac{dv_2}{dt} = sols[th2\_dd]$

$$\therefore S = (\theta_1, v_1, \theta_2, v_2)$$

```python
# The derivative function wjich depends upon S and t and contains all information regatding the 4 differential eqations.
def dSdt(S, t):
    return [ deriv1_th1_f(S[1]), #d(theta)/dt
            deriv2_th1_f(9.81, 2, 1, S[0], S[2], S[1], S[3]), # (dv1/dt)
            deriv1_th2_f(S[3]),  #d(theta2}/dt
            deriv2_th2_f(9.81, 2, 1, S[0], S[2], S[1], S[3]) #dv2/dt
           ]

#Initial conditions
th1_0 = np.pi/2
v1_0 = 0
th2_0 = -np.pi/2
v2_0 = 0
S0 = [th1_0, v1_0, th2_0, v2_0]
```

```python
# solving the equations 10001 times from 0 to 40
t = np.linspace(0, 40, 1001)
solution = odeint(dSdt, y0 = S0, t = t)
```

```python
# Required solutions
theta1 = solution.T[0]
theta2 = solution.T[2]
```

```python
# The coordinates of the 2 pendulums which will be used to plot the data
x1_pts = x1_f(theta1)
x2_pts = x2_f(theta1, theta2)
y1_pts = y1_f(theta1)
y2_pts = y2_f(theta1, theta2)
```

```python
plt.plot(x2_pts, y2_pts)
plt.plot(x1_pts, y1_pts)
```

```python
# An animation of the above system
# The number of frames are the number of datapoints in between each second. So, this animation is in real time.
fig, ax = plt.subplots(1,1, figsize=(12,10))
ax.set_facecolor('w')
line, = plt.plot([], [], 'ro-', lw=2, markersize=8)
ax.set(ylim=(-4,4), xlim=(-4,4))
def animate(i):
    line.set_data([0, x1_pts[i], x2_pts[i]], [0, y1_pts[i], y2_pts[i]])
    return line,
anim = FuncAnimation(fig, animate, frames=1000, interval=50)
anim.save('double_pendulum.gif',writer='pillow',fps=25)
```


# Kozeny-Carman equation

\begin{equation}
    K = \dfrac{d_p^2}{180}\dfrac{\theta^3}{(1-\theta)^2} \dfrac{\rho g }{\mu}
\end{equation}

```python
%reset -f
```

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

#Globals
rho = 1000.  #kg/m3
g   = 9.81   #cm/s2
mu  = 0.001  #Ns/m2
dp  = 4.4E-4 #m

def KozenyCarman(theta):
    return dp**2 * theta**3 * rho * g / (180 * (1-theta)**2 * mu)

def Chen2010(theta):
    perm = 810.0*(theta)**3.1   # Original equation
    perm = perm * 9.869233E-13         # Darcy >> m2
    Kcon = perm * rho * g / mu  # Permeability >> Hydraulic cond.
    return Kcon

def Chen2009(theta):
    perm = 1600.0*(theta)**3.7   # Original equation
    perm = perm * 9.869233E-13         # Darcy >> m2
    Kcon = perm * rho * g / mu  # Permeability >> Hydraulic cond.
    return Kcon

def findTheta(K_expected=1.0E-8):

    def minimizer(theta):
        K_init = KozenyCarman(theta)
        return (K_init - K_expected)**2

    solution = root(minimizer,0.1)
    print(solution.message + f" >> Porosity = {solution.x}")
    return solution.x
```

```python
porosity = np.linspace(0.001,0.5,100)
hydrCond = KozenyCarman(porosity)
```

```python
fig,ax = plt.subplots(figsize=(8,5),facecolor="white");

ax.plot(porosity,KozenyCarman(porosity),lw=3,c="blue",label='Kozeny-Carman')
ax.plot(porosity,Chen2010(porosity),lw=3,c="red",label="Chen2010")
ax.plot(porosity,Chen2009(porosity),lw=3,c="pink",label="Chen2009")
ax.axvspan(xmin=10**-0.8,xmax=10**-0.35,color='pink',alpha=0.5)
ax.set(yscale='log',xscale='log')
ax.set_xlabel("Porosity $\\theta$ ")
ax.set_ylabel("Hydraulic conductivity \n$K$ [m/s]")
ax.axhline(y=1.0E-8,lw=1,ls='dotted')
ax.legend()
plt.show()
```

```python
theta2 = findTheta(1.0E-7)
```

    The solution converged. >> Porosity = [0.02086702]

```python
print("{:.4E} m/s".format(KozenyCarman(0.35)))
```

    1.0707E-03 m/s

```python
from jupypft import attachmentRateCFT
```

```python
katt,_ = attachmentRateCFT.attachmentRate(dp=1.0E-7,dc=4.4E-4,
    q=0.35E-3,
    theta=0.35,
    visco=0.001,
    rho_f=1000.,
    rho_p=1050.0,
    A=1.0E-20,
    T=298.0,
    alpha=0.0043273861959162,
    debug=True)
```

    Diffusion coeff:  4.3654E-12
    Darcy velocity:   3.5000E-04
    Pore-water vel:   1.0000E-03
    ---
    Happel parameter: 5.2527E+01
    NR number:        2.2727E-04
    NPe number:       3.5277E+04
    NvW number:       2.4305E+00
    NGr number:       3.1211E-06
    ---
    etaD collector:   1.0409E-02
    etaI collector:   1.9641E-05
    etaG collector:   2.8626E-07
    eta0 collector:   1.0429E-02
    ---
    Attach rate   :   1.0000E-04

```python
"{:.6E}".format(0.0043273861959162)
```

    '4.327386E-03'

```python
1.0E-4/katt
```

    1.0000000000000002

```python

```


```python
import sympy as sm
```

## Depth

```python
u1, u2, r, k, mu_b, d = sm.symbols("u1, u2, r, k, mu_b, d", real=True)
mu = sm.sqrt(1 - r ** 2)
ir = 1 - u1 * (1 - mu) - u2 * (1 - mu) ** 2
```

```python
f0 = sm.simplify(sm.integrate(2 * sm.pi * r * ir, (r, 0, 1)))
f0
```

```python
df = sm.pi * k ** 2 * ir.subs([(r, sm.sqrt(1 - mu_b ** 2))])
df
```

```python
delta = sm.simplify(df / f0)
delta
```

```python
sm.solve(sm.Eq(delta, d), k ** 2)[0]
```

## Duration

```python
# sin2phi = sin^2(pi * tau / P)

k, b, a, sin2phi, tau, P = sm.symbols("k, b, a, sin2phi, tau, P", real=True)

cos2i = (b / a) ** 2
sin2i = 1 - cos2i

val = sm.simplify(((1 + k) ** 2 - b ** 2) / (a ** 2 * sin2i))
val
```

```python
a2_res = sm.solve(sm.Eq(sin2phi, val), a ** 2)[0]
a2_res
```

```python
sm.simplify(a2_res.subs([(sin2phi, sm.sin(sm.pi * tau / P) ** 2)]))
```

```python
sm.simplify(a2_res - ((1 + k) ** 2 - b ** 2 * (1 - sin2phi)) / sin2phi)
```

```python
sm.simplify(sm.diff(a2_res.subs([(sin2phi, sm.sin(sm.pi * tau / P) ** 2)]), tau))
```

```python

```


# Apply different Kernels

#### In Covolutional Neural Network (CNN), the images pass through different type of filters, in purpose to extract features.
#### In this simple notebook, I tried to apply different kind of kernels (filters), as an attempt to totally absorb the concept of convolution, and to know how CNN extracts its own features from images!

```python
from PIL import Image
from numpy import array
import matplotlib.pyplot as plt
from scipy import signal
from scipy import misc
import numpy as np
import cv2
```

```python
img = cv2.imread("images/cat.jpg",0)
img.shape
```

    (190, 265)

#### This function takes the image's martix, and the kernel's as well, the convolve them using 'signal.convolve2d'

```python
def DisplayImagesBeforeAfterConvolution(image, kernel):
    plt.figure(figsize=(10,10))
    plt.subplot(121),plt.imshow(image),plt.title('Original')
    plt.xticks([]), plt.yticks([])

    filtered_image1 = signal.convolve2d(image,kernel)

    plt.subplot(122),plt.imshow(filtered_image1),plt.title('Filtered')
    plt.xticks([]), plt.yticks([])
    plt.show()

```

### 1- Horizontal edge detection kernel
\begin{equation}
  \begin{bmatrix}
    -1 & -2 & -1 \\
    0 & 0 & 0 \\
    1 & 2 & 1
  \end{bmatrix}
  \label{eq:aeqn}
\end{equation}

```python
kernel1 = np.array([[-1,-2,-1],[0,0,0],[1,2,1]])

```

```python
DisplayImagesBeforeAfterConvolution(img, kernel1)
```

### 2- Vertical edge detection kernel
\begin{equation}
  \begin{bmatrix}
    -1 & 0 & 1 \\
    -2 & 0 & 2 \\
    -1 & 0 & 1
  \end{bmatrix}
  \label{eq:aeqn}
\end{equation}

```python
kernel2= np.array([[-1,0,1],[-2,0,2],[-1,0,1]])
DisplayImagesBeforeAfterConvolution(img, kernel2)
```

### 3- Inverse
\begin{equation}
  \begin{bmatrix}
    -1 & -1 & -1 \\
    -1 & -1 & -1 \\
    -1 & -1 & -1
  \end{bmatrix}
  \label{eq:aeqn}
\end{equation}

```python
kernel4= np.array([[-1,-1,-1],[-1,-1,-1],[-1,-1,-1]])
DisplayImagesBeforeAfterConvolution(img, kernel4)
```

### 4- Sharpness kernel
\begin{equation}
  \begin{bmatrix}
    0 & -1 & 0 \\
    -1 & 5 & -1 \\
    0 & -1 & 0
  \end{bmatrix}
  \label{eq:aeqn}
\end{equation}

```python
kernel4= np.array([[0,-1,0],[-1,5,-1],[0,-1,0]])
DisplayImagesBeforeAfterConvolution(img, kernel4)
```

### 5- Smoothness kernel
\begin{equation}
  \begin{bmatrix}
    1/9 & 1/9 & 1/9 \\
    1/9 & 1/9 & 1/9 \\
    1/9 & 1/9 & 1/9
  \end{bmatrix}
  \label{eq:aeqn}
\end{equation}

```python
kernel5= np.array([[1,1,1],[1,1,1],[1,1,1]])*1/9
DisplayImagesBeforeAfterConvolution(img, kernel5)
```

### 6- Random Karnel

```python
kernel6= np.array([[-4,5,-6],[-2,-3,14],[-6,1,-8]])*1/9
DisplayImagesBeforeAfterConvolution(img, kernel)
```

```python

```


# Random Signals

*This jupyter notebook is part of a [collection of notebooks](../index.ipynb) on various topics of Digital Signal Processing. Please direct questions and suggestions to [Sascha.Spors@uni-rostock.de](mailto:Sascha.Spors@uni-rostock.de).*

## Ensemble Averages

Ensemble averages characterize the average properties of a random process across the population of all possible sample functions in the ensemble. We distinguish between first- and higher-order ensemble averages. The former consider the average properties of the sample functions of one random process for one particular time-instant $k$, while the latter take more than one random process at different time-instants into account.

### First-Order Ensemble Averages

#### Definition

The first-order ensemble average of a continuous-amplitude real-valued random signal $x[k]$ is defined as

\begin{equation}
E\{ f(x[k]) \} = \lim_{N \to \infty} \frac{1}{N} \sum_{n=0}^{N-1} f(x_n[k])
\end{equation}

where $E\{ \cdot \}$ denotes the expectation operator, $x_n[k]$ the $n$-th sample function and $f(\cdot)$ an arbitrary mapping function. It is evident from the definition that the ensemble average can only be given exactly for random processes where the internal structure is known. For practical random processes, like e.g. speech, the ensemble average can only be approximated by a finite but sufficiently large number $N$ of sample functions.

The ensemble average can also be given in terms of the univariate probability density function (PDF)

\begin{equation}
E\{ f(x[k]) \} = \int\limits_{-\infty}^{\infty} f(\theta) \, p_x(\theta, k) \, \mathrm{d}\theta
\end{equation}

#### Properties

The following properties can be concluded from the definition of the ensemble average:

1. The ensemble averages for two different time-instants $k_1$ and $k_2$ differ in general

    \begin{equation}
    E\{ f(x[k_1]) \} \neq E\{ f(x[k_2]) \}
    \end{equation}

2. For a linear mapping $f(x[k]) = C \cdot x[k]$ with $C \in \mathbb{R} \setminus \{0\}$, the ensemble average is a linear operation

    \begin{equation}
    E\{ A \cdot x[k] + B \cdot y[k] \} = A \cdot E\{ x[k] \} + B \cdot E\{ y[k] \}
    \end{equation}

3. For a deterministic signal $x_n[k] = s[k]$ $\forall n$ the ensemble average is

    \begin{equation}
    E\{ f(s[k]) \} = f(s[k])
    \end{equation}

The choice of the mapping function $f(\cdot)$ determines the particular property of the random process which is characterized by the ensemble average. Common choices are discussed in the following.

#### Linear mean

The linear [mean](https://en.wikipedia.org/wiki/Mean), which is given for $f(x[k]) = x[k]$, is the arithmetic mean value across all sample functions $x_n[k]$ for a given time instant $k$.

Introducing $f(x[k]) = x[k]$ into the definition of the ensemble average yields

\begin{equation}
\mu_x[k] = E\{ x[k] \} = \int\limits_{-\infty}^{\infty} \theta \, p_x(\theta, k) \, \mathrm{d}\theta
\end{equation}

where $\mu_x[k]$ commonly denotes the linear mean. A process with $\mu_x[k] = 0$ is termed as *zero-mean* or *mean-free*. Note that $\mu_x$ should not be confused with the discrete frequency index of the DFT.

#### Quadratic mean

The quadratic mean is given for $f(x[k]) = x^2[k]$

\begin{equation}
E\{ x^2[k] \} = \int\limits_{-\infty}^{\infty} \theta^2 \, p_x(\theta, k) \, \mathrm{d}\theta
\end{equation}

It quantifies the average instantaneous power of a sample function for a given time index $k$.

#### Variance

The [variance](https://en.wikipedia.org/wiki/Variance) is defined as the quadratic mean of a zero-mean random process. For a general random process with linear mean $\mu_x$ it is given as

\begin{equation}
\sigma_x^2[k] = E\{ (x[k] - \mu_x[k])^2 \}
\end{equation}

where $\sigma_x^2[k]$ commonly denotes the variance, $\sigma_x[k]$ is known as the [standard deviation](https://en.wikipedia.org/wiki/Standard_deviation). Note that the subtraction of the linear mean $\mu_x$ from the samples $x_n[k]$ makes a random signal mean-free. The variance characterizes how far the amplitude values of a random signal are spread out from its mean value.

The variance can be given in terms of the linear and quadratic mean as

\begin{equation}
\sigma_x^2[k] = E\{ x^2[k] \} - \mu_x^2[k]
\end{equation}

**Excercise**

* Derive the relation above from the definitions and properties of the first order ensemble average.

#### Example - Linear and quadratic mean, and variance of a random process

The following example computes and plots the linear $\mu_x$ and quadratic mean, and variance $\sigma_x^2$ of a random process. Since in practice only a limited number $N$ of sample functions can be evaluated numerically the true values of these quantities are only approximated/estimated. Estimated quantities are denoted by a 'hat' over the respective quantity, e.g. $\hat{\mu}_x[k]$.

```python
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt

K = 64  # number of random samples
N = 1000  # number of sample functions

# generate the sample functions
np.random.seed(5)
x = np.random.normal(size=(N, K))
x += np.tile(np.cos(2*np.pi/K*np.arange(K)), [N, 1])

# estimate the linear mean as ensemble average
mu = 1/N * np.sum(x, 0)
# estimate the quadratic mean
qu = 1/N * np.sum(x**2, 0)
# estimate the variance
sigma = 1/N * np.sum((x-mu)**2, 0)

# plot results
plt.rc('figure', figsize=(10, 3))

plt.figure()
plt.stem(x[0, :])
plt.title(r'Sample function $x_0[k]$')
plt.xlabel(r'$k$')
plt.ylabel(r'$x_0[k]$')
plt.axis([0, K, -3, 3])

plt.figure()
plt.stem(mu)
plt.title(r'Estimate of linear mean')
plt.xlabel(r'$k$')
plt.ylabel(r'$\hat{\mu}_x[k]$')
plt.axis([0, K, -1.5, 1.5])

plt.figure()
plt.stem(qu)
plt.title(r'Estimate of quadratic mean')
plt.xlabel(r'$k$')
plt.ylabel(r'$\hat{E}\{x^2[k]\}$')
plt.axis([0, K, 0, 2.5])

plt.figure()
plt.stem(sigma)
plt.title(r'Estimate of variance')
plt.xlabel(r'$k$')
plt.ylabel(r'$\hat{\sigma}^2_x[k]$')
plt.axis([0, K, 0, 1.5]);
```

**Exercise**

* What does the linear and quadratic mean, and the variance tell you about the average behavior of the sample functions?
* Change the number `N` of sample functions and rerun the example. What influence has a decrease/increase of the sample functions on the estimated ensemble averages?

Inspection of the estimated linear mean reveals that in average the sample functions follow a cosine function with respect to the sample index $k$. The variance shows that the amount of which the samples of the sample functions for one particular time-instant $k$ are spread around the linear mean is constant. The estimate of the quadratic mean is given as $\hat{E}\{ x^2[k] \} = \hat{\mu}_x^2[k] + \hat{\sigma}_x^2[k]$. The higher the number $N$ of sample functions used for the estimate of the ensemble averages, the lower the uncertainty in comparison to the true values becomes.

### Second-Order Ensemble Averages

#### Definition

The second-order ensemble average of two continuous-amplitude real-valued random signals $x[k]$ and $y[k]$ is defined as

\begin{equation}
E\{ f(x[k_x], y[k_y]) \} := \lim_{N \to \infty} \frac{1}{N} \sum_{n=0}^{N-1} f(x_n[k_x], y_n[k_y])
\end{equation}

It can be given in terms of the bivariate PDF as

\begin{equation}
E\{ f(x[k_x], y[k_y]) \} = \iint\limits_{-\infty}^{\infty} f(\theta_x, \theta_y) \, p_{xy}(\theta_x, \theta_y, k_x, k_y) \, \mathrm{d}\theta_x\, \mathrm{d}\theta_y
\end{equation}

By setting $y[k] = x[k]$, the second-order ensemble average can also be used to characterize the average properties of the sample functions at two different time-instants $k_1$ and $k_2$.

The definition of the second-order ensemble average can be extended straightforward to the case of more than two random variables. The resulting ensemble average is termed as higher-order ensemble average.

The choice of the mapping function $f(\cdot)$ determines the particular property of the random process which is characterized by the ensemble average. The case of a linear mapping is discussed in the following.

#### Cross-correlation function

The [cross-correlation function](https://en.wikipedia.org/wiki/Cross-correlation) (CCF) of two random signals $x[k]$ and $y[k]$ is defined as the second-order ensemble average for a linear mapping $f(x[k_x], y[k_y]) = x[k_x] \cdot y[k_y]$

\begin{equation}
\varphi_{xy}[k_x, k_y] = E\{ x[k_x] \cdot y[k_y] \}
\end{equation}

It characterizes the statistical dependencies of two random signals $x[k]$ and $y[k]$ at two different time instants $k_x$ and $k_y$.

#### Auto-correlation function

The [auto-correlation function](https://en.wikipedia.org/wiki/Autocorrelation) (ACF) of a random signal $x[k]$ is defined as the second-order ensemble average for a linear mapping $f(x[k_x], y[k_y]) = x[k_1] \cdot x[k_2]$

\begin{equation}
\varphi_{xx}[k_1, k_2] = E\{ x[k_1] \cdot x[k_2] \}
\end{equation}

It characterizes the average statistical dependencies between the samples of a random signal $x[k]$ at two different time instants $k_1$ and $k_2$.

#### Example - Auto-correlation function of a random signal

The following example computes the ACF of a random signal $x[k]$ as ensemble average. The ACF is plotted as image where the colors denote the level of the ACF $\varphi_{xx}[k_1, k_2]$ for given time-instants $k_1$ and $k_2$.

```python
L = 64  # number of random samples
N = 1000  # number of sample functions

# generate sample functions
np.random.seed(1)
r = np.random.normal(size=(N, L))
h = np.random.normal(size=(N, 10))
x = np.asarray([np.convolve(r[n,:], h[n,:], mode='same') for n in range(N)]) \
    + np.tile(np.cos(2*np.pi/L*np.arange(L)), [N,1])

# estimate the auto-correlation function
acf = np.zeros((L, L))
for n in range(L):
    for m in range(L):
        acf[n, m] = 1/N * np.sum(x[:, n]*x[:, m], 0)

plt.figure(figsize = (7, 5))
plt.pcolor(np.arange(L), np.arange(L), acf, vmin=-2, vmax=2)
plt.title(r'Estimate of ACF $\hat{\varphi}_{xx}[k_1, k_2]$')
plt.xlabel(r'$k_1$')
plt.ylabel(r'$k_2$')
plt.colorbar()
plt.axis('tight');
```

**Exercise**

* Can you explain the specific symmetry of the ACF?

Inspection of the definition of the ACF $\varphi_{xx}[k_1, k_2] = \lim_{N \to \infty} \frac{1}{N} \sum_{n=0}^{N-1} x_n[k_1] \cdot x_n[k_2]$ reveals that the sample indexes $k_1$ and $k_2$ can be interchanged without changing the value of the ACF. Hence $\varphi_{xx}[k_1, k_2] = \varphi_{xx}[k_2, k_1]$. This symmetry can be observed in above plot.

**Copyright**

This notebook is provided as [Open Educational Resource](https://en.wikipedia.org/wiki/Open_educational_resources). Feel free to use the notebook for your own purposes. The text is licensed under [Creative Commons Attribution 4.0](https://creativecommons.org/licenses/by/4.0/), the code of the IPython examples under the [MIT license](https://opensource.org/licenses/MIT). Please attribute the work as follows: *Sascha Spors, Digital Signal Processing - Lecture notes featuring computational examples, 2016-2017*.


# Example 5.3

Import necessary packages

```python
import numpy as np
from MOT_2dim2assets import *
from MOT_2dim3assets import *
import matplotlib.pyplot as plt
from matplotlib import rc
from matplotlib import style
from matplotlib.ticker import (AutoMinorLocator, MultipleLocator)
style.use('ggplot')
from mpl_toolkits.mplot3d import axes3d, Axes3D
print("Necessary Packages imported")
```

    Necessary Packages imported

Define the relevant payoff function
\begin{align}
&c_{1,K}(S_{t_1}^1,S_{t_2}^1,S_{t_1}^2,S_{t_2}^2)=\left(K-\max_{i,k}\{S_{t_i}^k\}\right)_+
\end{align}

```python
def c_1(s_11,s_12,s_21,s_22,K=10):
    return np.max([K-np.max([s_11,s_12,s_21,s_22]),0])
```

Define the marginals:
\begin{align*}
&S_{t_1}^1 \sim \mathcal{U}(\{8,10,12\}),\\
&S_{t_2}^1 \sim \mathcal{U}(\{7,9,11,13\}),\\
&S_{t_1}^2 \sim \mathcal{U}(\{8,10,12\}),\\
&S_{t_2}^2 \sim \mathcal{U}(\{4,7,10,13,16\}),\\
\end{align*}

```python
# First Security
p11 = np.repeat(1/3,3)
v11 = [8,10,12]
p21 = np.repeat(1/4,4)
v21 = [7,9,11,13]
# Second Security
p12 = np.repeat(1/3,3)
v12 = [8,10,12]
p22 = np.repeat(1/5,5)
v22 = [4,7,10,13,16]
```

## Remark

Let prices of $1_{\{\max\{S_{t_i}^k,S_{t_j}^l\} \leq x\}}$ be given for all $x$, i.e., we know a function $\pi$ such that
$$
\pi(x)=\mathbb{E}_{\mathbb{Q}}\left[1_{\{\max\{S_{t_1}^1,S_{t_1}^2\} \leq x\}}\right] \text{ for all } x \in \mathbb{R}.
$$
Then the above equation is equivalent to
$$
\pi(x)=\mathbb{E}_{\mathbb{Q}}\left[1_{\{\max\{S_{t_1}^1,S_{t_1}^2\} \leq x\}}\right]\text{ for all } x \in \operatorname{supp}(S_{t_i}^k)\cup \operatorname{supp}(S_{t_j}^l).
$$
with
$$
1=\pi(x^*)=\mathbb{E}_{\mathbb{Q}}\left[1_{\{\max\{S_{t_1}^1,S_{t_1}^2\} \leq x^*\}}\right]\text{ for } x^* =\max\left\{\operatorname{supp}(S_{t_i}^k)\cup \operatorname{supp}(S_{t_j}^l)\right\}
$$

## Computation of the prices of $c_1$ in dependence of price information on digital options(Linear Programming)

Compute Prices of $c_1$ in dependence of price information on prices of $1_{\{\max\{S_{t_1}^1,S_{t_1}^2\} \leq 8\}}$ and $1_{\{\max\{S_{t_1}^1,S_{t_1}^2\} \leq 10\}}$. This is because of the structure of the marginals equivalent to the knowledge of $1_{\{\max\{S_{t_1}^1,S_{t_1}^2\} \leq x\}}$ for all $x$ (since $\mathbb{E}_{\mathbb{Q}}\left[1_{\{\max\{S_{t_1}^1,S_{t_1}^2\} \leq 12\}}\right] =1$ this is sufficient)

Computation of the grid

```python
index_set = [[1,1,1,2]]
copula_strikes = [[8,10]]
# No of discretization Points (of the grid)
N = 20
# Initialize Grid
possible_prices_1 = np.linspace(0,0.27,N)
possible_prices_2 = np.linspace(0.3,0.7,N)
grid1_9 = np.zeros((N,N))
grid1_11 = np.zeros((N,N))
grid1_9_wo_martingale = np.zeros((N,N))
grid1_11_wo_martingale = np.zeros((N,N))

# define payoffs for fixed strikes
def c1_9(s_11,s_12,s_21,s_22):
    return c_1(s_11,s_12,s_21,s_22,9)
def c1_11(s_11,s_12,s_21,s_22):
    return c_1(s_11,s_12,s_21,s_22,11)

for i in range(N):
    for j in range(N):
        copula_prices_var = [[possible_prices_1[i],possible_prices_2[j]]]
        try:
            grid1_9[i,j] = opt_plan_discrete_multiasset(v11,p11,v12,p12,v21,p21,v22,p22,func=c1_9,minimize=False,
                                         copula_indices = index_set,
                                         copula_strikes=copula_strikes,
                                         copula_prices=copula_prices_var)[0]
            grid1_9_wo_martingale[i,j] = opt_plan_discrete_multiasset(v11,p11,v12,p12,v21,p21,v22,p22,func=c1_9,minimize=False,
                                         copula_indices = index_set,
                                         copula_strikes=copula_strikes,
                                         copula_prices=copula_prices_var,martingale_condition=False)[0]
        except:
            pass

for i in range(N):
    for j in range(N):
        copula_prices_var = [[possible_prices_1[i],possible_prices_2[j]]]
        try:
            grid1_11[i,j] = opt_plan_discrete_multiasset(v11,p11,v12,p12,v21,p21,v22,p22,func=c1_11,minimize=False,
                                         copula_indices = index_set,
                                         copula_strikes=copula_strikes,
                                         copula_prices=copula_prices_var)[0]
            grid1_11_wo_martingale[i,j] = opt_plan_discrete_multiasset(v11,p11,v12,p12,v21,p21,v22,p22,func=c1_11,minimize=False,
                             copula_indices = index_set,
                             copula_strikes=copula_strikes,
                             copula_prices=copula_prices_var,martingale_condition=False)[0]
        except:
            pass
```

# Plot of the results

```python
# LEGEND

first = [1]
second = [1]
third = [1]
fourth = [1]
style.use('ggplot')
plt.figure(figsize=(7.5,1))
plt.plot(first,linestyle='solid',color = "darkred")
plt.plot(second,linestyle='solid',color = "darkblue")
plt.plot(third,linestyle='dashed',color = "salmon")
plt.plot(fourth,color = "cornflowerblue")

plt.legend(['Upper price bound without martingale property',
            'Upper price bound with martingale property',
            'Upper price bound with information on digital options without martingale property',
            'Upper price bound with information on digital options with martingale property'],
           loc='right')
plt.axis('off')
plt.savefig('eps/discrete_copula_info_legend.eps', format='eps')

# 3D PLOT
x = possible_prices_1
y = possible_prices_2
X, Y = np.meshgrid(x, y)

# creating a smaller grid to display the upper bounds
x_small = np.linspace(0,0.27,3)
y_small = np.linspace(0.3,0.7,3)
X_small, Y_small = np.meshgrid(x_small, y_small)

style.use('default')
fig = plt.figure(figsize=(10,9))
ax = fig.add_subplot(1, 2, 1, projection='3d')

ax.plot_wireframe(X_small, Y_small , np.ones((3,3))*np.max(np.max(grid1_9)),color = "darkblue")
ax.plot_wireframe(X_small, Y_small , np.ones((3,3))*np.max(np.max(grid1_9_wo_martingale)),color = "darkred")
ax.plot_surface(X, Y , grid1_9,color = "cornflowerblue")
ax.plot_wireframe(X, Y , grid1_9_wo_martingale,color = "salmon")
ax.view_init(20,  220)
ax.set_xlabel(r'$p_1^{12}(8)$')
ax.set_ylabel(r'$p_1^{12}(10)$')
ax.set_zlabel("Upper Price Bound of " + r'$c_{1,9}$')

ax = fig.add_subplot(1, 2, 2, projection='3d')

ax.plot_wireframe(X_small, Y_small , np.ones((3,3))*np.max(np.max(grid1_11)),color = "darkblue")
ax.plot_wireframe(X_small, Y_small , np.ones((3,3))*np.max(np.max(grid1_11_wo_martingale)),color = "darkred")
ax.plot_surface(X, Y , grid1_11,color = "cornflowerblue")
ax.plot_wireframe(X, Y , grid1_11_wo_martingale,color = "salmon")
ax.view_init(20,  220)
ax.set_xlabel(r'$p_1^{12}(8)$')
ax.set_ylabel(r'$p_1^{12}(10)$')
ax.set_zlabel("Upper Price Bound of " + r'$c_{1,11}$')
# Save the plot
plt.savefig('eps/discrete_copula_info.eps', format='eps',bbox_inches='tight')
plt.show()
```


Implementation example
---

This is the actual notebook used to develop the calc_scf() method of the meshless.composite.laminate.Laminate class.

```python
import numpy as np
from sympy.abc import z

from meshless.composite.laminate import read_stack

E1 = 49.627e9
E2 = 15.43e9
nu12 = 0.38
G12 = 4.8e9
G13 = 4.8e9
G23 = 4.8e9
laminaprop = (E1, E2, nu12, G12, G13, G23)

tmap = {
      45: 0.143e-3,
     -45: 0.143e-3,
       0: 1.714e-3
    }
X = 1
angles = [-45, +45, 0, +45, -45, 0]*X + [0, -45, +45, 0, +45, -45]*X
plyts = [tmap[angle] for angle in angles]

lam = read_stack(angles, plyts=plyts, laminaprop=laminaprop)

def calc_scf(self):
    """Calculate shear correction factors

    Reference:

        Vlachoutsis, S. "Shear correction factors for plates and shells",
        Int. Journal for Numerical Methods in Engineering, Vol. 33,
        1537-1552, 1992.

        http://onlinelibrary.wiley.com/doi/10.1002/nme.1620330712/full

    Using "one shear correction factor" (see reference), assuming:

    - constant G13, G23, E1, E2, nu12, nu21 within each ply
    - g1 calculated using z at the middle of each ply
    - zn1 = Laminate.offset

    Returns
    -------

    k13, k23 : tuple
        Shear correction factors. Also updates attributes: `scf_k13`
        and `scf_k23`.

    """
    D1 = 0
    R1 = 0
    den1 = 0

    D2 = 0
    R2 = 0
    den2 = 0

    offset = self.offset
    zbot = -self.t/2 + offset
    z1 = zbot

    for ply in self.plies:
        z2 = z1 + ply.t
        e1 = (ply.matobj.e1 * np.cos(np.deg2rad(ply.theta)) +
              ply.matobj.e2 * np.sin(np.deg2rad(ply.theta)))
        e2 = (ply.matobj.e2 * np.cos(np.deg2rad(ply.theta)) +
              ply.matobj.e1 * np.sin(np.deg2rad(ply.theta)))
        nu12 = (ply.matobj.nu12 * np.cos(np.deg2rad(ply.theta)) +
              ply.matobj.nu21 * np.sin(np.deg2rad(ply.theta)))
        nu21 = (ply.matobj.nu21 * np.cos(np.deg2rad(ply.theta)) +
          ply.matobj.nu12 * np.sin(np.deg2rad(ply.theta)))

        D1 += e1 / (1 - nu12*nu21)
        R1 += D1*((z2 - offset)**3/3 - (z1 - offset)**3/3)
        g13 = ply.matobj.g13
        d1 = g13 * ply.t
        den1 += d1 * (self.t / ply.t) * D1**2*(15*offset*z1**4 + 30*offset*z1**2*zbot*(2*offset - zbot) - 15*offset*z2**4 + 30*offset*z2**2*zbot*(-2*offset + zbot) - 3*z1**5 + 10*z1**3*(-2*offset**2 - 2*offset*zbot + zbot**2) - 15*z1*zbot**2*(4*offset**2 - 4*offset*zbot + zbot**2) + 3*z2**5 + 10*z2**3*(2*offset**2 + 2*offset*zbot - zbot**2) + 15*z2*zbot**2*(4*offset**2 - 4*offset*zbot + zbot**2))/(60*g13)

        D2 += e2 / (1 - nu12*nu21)
        R2 += D2*((z2 - self.offset)**3/3 - (z1 - self.offset)**3/3)
        g23 = ply.matobj.g23
        d2 = g23 * ply.t
        den2 += d2 * (self.t / ply.t) * D2**2*(15*offset*z1**4 + 30*offset*z1**2*zbot*(2*offset - zbot) - 15*offset*z2**4 + 30*offset*z2**2*zbot*(-2*offset + zbot) - 3*z1**5 + 10*z1**3*(-2*offset**2 - 2*offset*zbot + zbot**2) - 15*z1*zbot**2*(4*offset**2 - 4*offset*zbot + zbot**2) + 3*z2**5 + 10*z2**3*(2*offset**2 + 2*offset*zbot - zbot**2) + 15*z2*zbot**2*(4*offset**2 - 4*offset*zbot + zbot**2))/(60*g23)

        z1 = z2

    self.scf_k13 = R1**2 / den1
    self.scf_k23 = R2**2 / den2

    return self.scf_k13, self.scf_k23

print(calc_scf(lam))
```

    (0.76833028877157949, 0.77602930040164342)

Integrating symbolically the denominator expressions for K13 and K23

```python
from sympy.abc import z
import sympy

sympy.var('z1, z2, D1, offset, zbot, g13')
g1 = - D1*sympy.integrate(z - offset, (z, zbot, z))
expr = sympy.integrate((g1**2 / g13), (z, z1, z2))
print(expr.simplify())
```

    D1**2*(15*offset*z1**4 + 30*offset*z1**2*zbot*(2*offset - zbot) - 15*offset*z2**4 + 30*offset*z2**2*zbot*(-2*offset + zbot) - 3*z1**5 + 10*z1**3*(-2*offset**2 - 2*offset*zbot + zbot**2) - 15*z1*zbot**2*(4*offset**2 - 4*offset*zbot + zbot**2) + 3*z2**5 + 10*z2**3*(2*offset**2 + 2*offset*zbot - zbot**2) + 15*z2*zbot**2*(4*offset**2 - 4*offset*zbot + zbot**2))/(60*g13)


# Valuación de opciones con barrera

## 1. Recorderis de valuación usando simulación...

```python
#importar los paquetes que se van a usar
import pandas as pd
import pandas_datareader.data as web
import numpy as np
from sklearn.neighbors import KernelDensity
import datetime
from datetime import datetime, timedelta
import scipy.stats as stats
import scipy as sp
import scipy.optimize as optimize
import scipy.cluster.hierarchy as hac
import matplotlib.pyplot as plt
import seaborn as sns
%matplotlib inline
#algunas opciones para Python
pd.set_option('display.notebook_repr_html', True)
pd.set_option('display.max_columns', 6)
pd.set_option('display.max_rows', 10)
pd.set_option('display.width', 78)
pd.set_option('precision', 3)
```

```python
def get_historical_closes(ticker, start_date, end_date=None):
    #closes = web.YahooDailyReader(ticker, start_date, end_date).read().sort_index('major_axis')
    closes = web.YahooDailyReader(symbols=ticker, start=start_date, end=end_date).read()
    closes.set_axis(closes.loc['date',:,ticker[0]].values, axis=1, inplace=True)
    closes = closes.loc['adjclose'].sort_index().dropna()
    closes = pd.DataFrame(np.array(closes.as_matrix(), dtype=np.float64), columns=ticker, index=closes.index)
    closes.index.name = 'Date'
    #return closes.loc['Adj Close']
    return closes
####### Calculation of log-returns
def calc_daily_ret(closes):
    return np.log(closes/closes.shift(1)).dropna()
```

```python
closes_aapl = get_historical_closes(['AAPL'], '2017-01-01')
```

```python
ret_aapl = calc_daily_ret(closes_aapl)
```

### 1.1 Modelo normal de rendimientos

```python
mu = ret_aapl.mean().AAPL
sigma = ret_aapl.std().AAPL
r = 0.0160/252
mu, sigma, r
```

    (0.0014979727368957033, 0.012018070762626952, 6.349206349206349e-05)

```python
K = 180
ndays = (datetime(2018,6,15).date()-datetime(2018,3,14).date()).days
nscen = 100000
dates = pd.date_range('2018-03-15',periods=ndays).astype('str')
sim_ret = pd.DataFrame(sigma*np.random.randn(ndays,nscen)+r,index=dates)
sim_closes = (closes_aapl.iloc[-1].AAPL)*np.exp(sim_ret.cumsum())
```

```python
strike = pd.DataFrame(K*np.ones(ndays*nscen).reshape((ndays,nscen)),index=dates)
call=pd.DataFrame({'Prima':np.exp(-r*ndays)*np.fmax(sim_closes-strike,np.zeros(ndays*nscen).reshape((ndays,nscen))).T.mean()},index=dates)
call.plot();
```

```python
call.iloc[-1]
```

    Prima    2.287
    Name: 2018-06-15, dtype: float64

```python
sigma_est = sim_closes.iloc[-1].std()
(call.iloc[-1].Prima-1.96*sigma_est*np.exp(-r*ndays)/np.sqrt(nscen),call.iloc[-1].Prima+1.96*sigma_est*np.exp(-r*ndays)/np.sqrt(nscen))
```

    (2.1575028956429336, 2.4159938482671373)

### 1.2 Histograma de rendimientos

```python
freq, values = np.histogram(ret_aapl-mu+r, bins=250)
freq = freq.astype('float32')
prob = freq/np.sum(freq)
```

```python
K = 180
ndays = (datetime(2018,6,15).date()-datetime(2018,3,14).date()).days
nscen = 100000
dates = pd.date_range('2018-03-15',periods=ndays).astype('str')
ret = np.random.choice(values[1:], (ndays,nscen), p = prob)
sim_ret_hist = pd.DataFrame(ret,index=dates)
sim_closes_hist = (closes_aapl.iloc[-1].AAPL)*np.exp(sim_ret_hist.cumsum())
```

```python
strike = pd.DataFrame(K*np.ones(ndays*nscen).reshape((ndays,nscen)), index=dates)
call_hist = pd.DataFrame({'Prima':np.exp(-r*ndays)*np.fmax(sim_closes_hist-strike,np.zeros(ndays*nscen).reshape((ndays,nscen))).T.mean()}, index=dates)
call_hist.plot();
```

```python
call_hist.iloc[-1]
```

    Prima    3.196
    Name: 2018-06-15, dtype: float64

```python
sigma_est = sim_closes_hist.iloc[-1].std()
(call_hist.iloc[-1].Prima-4.5*sigma_est*np.exp(-r*ndays)/np.sqrt(nscen),call_hist.iloc[-1].Prima+4.5*sigma_est*np.exp(-r*ndays)/np.sqrt(nscen))
```

    (2.8924930532708188, 3.5003836562021231)

### 1.3 Distribución empírica con kernel

```python
kde = KernelDensity(kernel='gaussian', bandwidth=0.001).fit(ret_aapl-mu+r)
```

```python
K = 180
ndays = (datetime(2018,6,15).date()-datetime(2018,3,14).date()).days
nscen = 100000
dates = pd.date_range('2018-03-15',periods=ndays).astype('str')
kde = KernelDensity(kernel='gaussian', bandwidth=0.001).fit(ret_aapl-mu+r)
ret = kde.sample(n_samples=ndays*nscen, random_state=None)
sim_ret_kernel = pd.DataFrame(ret.reshape((ndays,nscen)),index=dates)
sim_closes_kernel = (closes_aapl.iloc[-1].AAPL)*np.exp(sim_ret_kernel.cumsum())
```

```python
strike = pd.DataFrame(K*np.ones(ndays*nscen).reshape((ndays,nscen)), index=dates)
call_kernel = pd.DataFrame({'Prima':np.exp(-r*ndays)*np.fmax(sim_closes_kernel-strike,np.zeros(ndays*nscen).reshape((ndays,nscen))).T.mean()}, index=dates)
call_kernel.plot();
```

```python
call_kernel.iloc[-1]
```

    Prima    2.343
    Name: 2018-06-15, dtype: float64

```python
sigma_est = sim_closes_kernel.iloc[-1].std()
(call_kernel.iloc[-1].Prima-4.5*sigma_est*np.exp(-r*ndays)/np.sqrt(nscen),call_kernel.iloc[-1].Prima+4.5*sigma_est*np.exp(-r*ndays)/np.sqrt(nscen))
```

    (2.0445858444892302, 2.6412007000682003)

## 2. Aspectos teóricos

Para un activo con precio $S_t$, se considera una opción de la siguiente forma
\begin{equation}
    V_T=u(t)F_T
\end{equation}
para $0\leq t\leq T$. En este caso, $F_T=\max(S_T-K,0)$ para una opción tipo call o $F_T=\max(K-S_T,0)$ para una opción tipo put. La función $u(t)$ est\'a definida en $0\leq t\leq T$, toma el valor de uno sí se cumple cierta condición y el valor de cero en otra caso.

Por otra lado, para $B>0$ y $0<t<T$ se dice que $S_t$ ha superado la barrera $B$ sí $S_0B$ para algún tiempo $t$ en el periodo considerado. Similarmente, se dice que $S_t$ ha caído por debajo de la barrera $B$ sí $S_0>B$ y $S_t<B$ para alg\'un tiempo $t$ en el periodo considerado.

Finalmente, para un enunciado $x$, se define la función indicadora $I(x)$ como $I(x)=1$ si $x$ se cumple e $I(x)=0$ en otro caso.

Se esta forma, pueden definirse las siguientes acciones

* Up-and-out: Sí $S_0<B$, entonces $u(t)=I(S_tB$, entonces $u(t)=I(S_t>B)$.
* Up-and-in: Sí $S_0B)$.
* Down-and-in: Sí $S_0>B$, entonces $u(t)=I(S_t<B)$.

Así se se pueden definirse ocho nuevos tipos de opciones.

* Up-and-out call: Sí $S_0<B$, $C_t^{uo}=\max\{S_T-K,0\}I(S_tB$, $C_t^{do}=\max\{S_T-K,0\}I(S_t>B)$
* Up-and-in call: Sí $S_0B)$
* Down-and-in call: Sí $S_0>B$, $C_t^{di}=\max\{S_T-K,0\}I(S_t<B)$
* Up-and-out put: Sí $S_0<B$, $P_t^{uo}=\max\{K-S_T,0\}I(S_tB$, $P_t^{do}=\max\{K-S_T,0\}I(S_t>B)$
* Up-and-in put: Sí $S_0B)$
* Down-and-in put: Sí $S_0>B$, $P_t^{di}=\max\{K-S_T,0\}I(S_t<B)$

A las del tipo *up-and-out* y *down-and-out* se les conoce como opciones *knock-out*. De la misma forma, a las del tipo *up-and-in* y *down-and-in* se les conoce como opciones *knock-in*.

Note que estas son **opciones dependientes de la trayectoria del precio**, dado que $u(t)$ depende de $S_t$.

## 3. Valuación de una opción up and in

### 3.1 Modelo normal de rendimientos

```python
mu = ret_aapl.mean().AAPL
sigma = ret_aapl.std().AAPL
r = 0.0160/252
mu, sigma, r
```

    (0.0014979727368957033, 0.012018070762626952, 6.349206349206349e-05)

```python
K = 180
B = 190
ndays = (datetime(2018,6,15).date()-datetime(2018,3,14).date()).days
nscen = 100000
dates = pd.date_range('2018-03-15',periods=ndays).astype('str')
sim_ret = pd.DataFrame(sigma*np.random.randn(ndays,nscen)+r,index=dates)
sim_closes = (closes_aapl.iloc[-1].AAPL)*np.exp(sim_ret.cumsum())
```

Hasta la simulación, todo normal... falta crear la función indicadora

```python
barrier = pd.DataFrame(B*np.ones((ndays, nscen)),index=dates)
I = (sim_closes>barrier).cumsum()>0
I
```

      0
      1
      2
      ...
      99997
      99998
      99999

      2018-03-15
      False
      False
      False
      ...
      False
      False
      False

      2018-03-16
      False
      False
      False
      ...
      False
      False
      False

      2018-03-17
      False
      False
      False
      ...
      False
      False
      False

      2018-03-18
      False
      False
      False
      ...
      False
      False
      False

      2018-03-19
      False
      False
      False
      ...
      False
      False
      False

      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...

      2018-06-11
      False
      True
      True
      ...
      True
      False
      True

      2018-06-12
      False
      True
      True
      ...
      True
      False
      True

      2018-06-13
      False
      True
      True
      ...
      True
      False
      True

      2018-06-14
      False
      True
      True
      ...
      True
      False
      True

      2018-06-15
      False
      True
      True
      ...
      True
      False
      True

93 rows × 100000 columns

```python
strike = pd.DataFrame(K*np.ones((ndays, nscen)),index=dates)
call = pd.DataFrame({'Prima':np.exp(-r*ndays)*np.fmax(sim_closes-strike,np.zeros(ndays*nscen).reshape((ndays,nscen))).T.mean()},index=dates)
callui = pd.DataFrame({'Prima':np.exp(-r*ndays)*(I*np.fmax(sim_closes-strike,np.zeros((ndays,nscen)))).T.mean()},index=dates)
callui.plot();
```

```python
call.iloc[-1]
```

    Prima    8.673
    Name: 2018-06-15, dtype: float64

```python
callui.iloc[-1]
```

    Prima    8.471
    Name: 2018-06-15, dtype: float64

### 3.2 Distribución empírica con Kernel

```python
kde = KernelDensity(kernel='gaussian', bandwidth=0.001).fit(ret_aapl-mu+r)
```

```python
K = 180
B = 190
ndays = (datetime(2018,6,15).date()-datetime(2018,3,14).date()).days
nscen = 100000
dates = pd.date_range('2018-03-15',periods=ndays).astype('str')
kde = KernelDensity(kernel='gaussian', bandwidth=0.001).fit(ret_aapl-mu+r)
ret = kde.sample(n_samples=ndays*nscen, random_state=None)
sim_ret_kernel = pd.DataFrame(ret.reshape((ndays,nscen)),index=dates)
sim_closes_kernel = (closes_aapl.iloc[-1].AAPL)*np.exp(sim_ret_kernel.cumsum())
```

```python
barrier = pd.DataFrame(B*np.ones((ndays, nscen)),index=dates)
I = (sim_closes_kernel>barrier).cumsum()>0
I
```

      0
      1
      2
      ...
      99997
      99998
      99999

      2018-03-15
      False
      False
      False
      ...
      False
      False
      False

      2018-03-16
      False
      False
      False
      ...
      False
      False
      False

      2018-03-17
      False
      False
      False
      ...
      False
      False
      False

      2018-03-18
      False
      False
      False
      ...
      False
      False
      False

      2018-03-19
      False
      False
      False
      ...
      False
      False
      False

      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...

      2018-06-11
      False
      False
      True
      ...
      False
      False
      False

      2018-06-12
      False
      False
      True
      ...
      False
      False
      False

      2018-06-13
      False
      False
      True
      ...
      False
      False
      False

      2018-06-14
      False
      False
      True
      ...
      False
      False
      False

      2018-06-15
      False
      False
      True
      ...
      False
      False
      False

93 rows × 100000 columns

```python
strike = pd.DataFrame(K*np.ones((ndays,nscen)), index=dates)
call_kernel = pd.DataFrame({'Prima':np.exp(-r*ndays)*np.fmax(sim_closes_kernel-strike,np.zeros(ndays*nscen).reshape((ndays,nscen))).T.mean()},index=dates)
callui_kernel = pd.DataFrame({'Prima':np.exp(-r*ndays)*(I*np.fmax(sim_closes_kernel-strike,np.zeros((ndays,nscen)))).T.mean()},index=dates)
call_kernel.plot();
callui_kernel.plot();
```

```python
call_kernel.iloc[-1]
```

    Prima    8.655
    Name: 2018-06-15, dtype: float64

```python
callui_kernel.iloc[-1]
```

    Prima    8.442
    Name: 2018-06-15, dtype: float64

**Tarea.** Montar la simulación para una opción put down-and-in.

Created with Jupyter by Esteban Jiménez Rodríguez.