```python
import numpy as np
import torch

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

# %load_ext autoreload
# %autoreload 2
```

```python
import torch
import torch.utils.data
```

```python
# DGP related methods
import sys
sys.path.append('../..')

from dsvidgp.kernels import RBF
from dsvidgp.likelihoods import Gaussian
from dsvidgp.layers import SVGPLayer
from dsvidgp.initial_value import calc_median_of_pairwise_distance, calc_centroid
```

```python
RAND_SEED = 811
torch.manual_seed(RAND_SEED)
np.random.seed(RAND_SEED)
```

```python
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
device
```

    device(type='cpu')

```python
DATA_SIZE = 50
MINIBATCH_SIZE = 10

HIDDEN_LAYER_DIM = 3
INPUT_DIM = 1
OUTPUT_DIM = 1

# GP configurations
NUM_INDUCINGS = 20
GP_LEARNING_RATE = 0.01
DGP_NUM_EPOCHS = 1000

```

## Make data

```python

dx = np.random.rand(DATA_SIZE, 1).astype(np.float32) * 2.0 - 1.0
dy = np.where(dx + 0.04 * np.random.randn(*dx.shape) >= 0.0, 1.0, -1.0) \
        + 0.04 * np.random.randn(*dx.shape)
dy = dy.astype(np.float32)

nx = np.linspace(-1.0, 1.0, 200)[:, None]

plt.plot(dx, dy, 'o')
```

```python
class Dataset(torch.utils.data.Dataset):
    def __init__(self, dx, dy):

        self._N = len(dx)
        self._dx = dx
        self._dy = dy

    def __len__(self):
        return self._N

    def __getitem__(self, idx):
        return self._dx[idx], self._dy[idx]

dataset = Dataset(dx, dy)
dataloader = torch.utils.data.DataLoader(dataset, batch_size=MINIBATCH_SIZE, shuffle=True)

```

## Training method

```python
def train(optimizer, model, num_epochs):
    lower_bound_list = list()
    pred_mean_list = list()

    tx = torch.from_numpy(nx.astype(np.float32))

    pred_dstr = model.predict(tx.to(device))
    pred_mean_list.append(pred_dstr['mean'].data.cpu().numpy().copy())

    from ipywidgets import FloatProgress
    from IPython.display import display

    fp = FloatProgress(min=0, max=num_epochs)
    display(fp)

    for i in range(num_epochs):
        fp.value = i
        lower_bound = 0

        model.train()
        for b, (x_B, y_B) in enumerate(dataloader):
            x_B = x_B.to(device)
            y_B = y_B.to(device)

            optimizer.zero_grad()

            f_dstr = model(x_B)
            batch_bound = model.calc_bound(y_B, f_dstr)

            loss = -batch_bound  # maximize
            loss.backward()

            optimizer.step()

            lower_bound += batch_bound.data.cpu().item()

        lower_bound_list.append(lower_bound)

        model.eval()
        pred_dstr = model.predict(tx.to(device))
        pred_mean_list.append(pred_dstr['mean'].data.cpu().numpy().copy())

    return lower_bound_list, pred_mean_list

```

## Model definition

```python
class DGP3(torch.nn.Module):
    """3-layer deep Gaussian process model."""

    def __init__(self, initial_inducing_input,
                 initial_lengthscale, data_size):
        super().__init__()

        # layer 1
        kern1 = RBF(input_dim=INPUT_DIM,
                    initial_lengthscale=initial_lengthscale)
        self.layer1 = SVGPLayer(kernel=kern1,
                                input_dim=INPUT_DIM,
                                output_dim=HIDDEN_LAYER_DIM,
                                initial_inducing_input=initial_inducing_input,
                                initial_q_S_value=0.01,
                                fix_inducing=False)

        # define a function that copies 1-dim value to 1st dimension.
        lin = torch.zeros((INPUT_DIM, HIDDEN_LAYER_DIM))
        lin[:, 0] = 1.0
        self.mean_func1 = lambda x: torch.mm(x, lin)

        # layer 2
        kern2 = RBF(input_dim=HIDDEN_LAYER_DIM,
                    initial_lengthscale=initial_lengthscale)
        self.layer2 = SVGPLayer(kernel=kern1,
                                input_dim=HIDDEN_LAYER_DIM,
                                output_dim=HIDDEN_LAYER_DIM,
                                num_inducings=NUM_INDUCINGS,
                                initial_q_S_value=0.01,
                                fix_inducing=True)

        self.mean_func2 = lambda x: x

        # layer 3
        kern3 = RBF(input_dim=HIDDEN_LAYER_DIM,
                    initial_lengthscale=initial_lengthscale)
        self.layer3 = SVGPLayer(kernel=kern3,
                                input_dim=HIDDEN_LAYER_DIM,
                                output_dim=OUTPUT_DIM,
                                num_inducings=NUM_INDUCINGS,
                                initial_q_S_value=1.0,
                                fix_inducing=True)

        # output
        self.likelihood = Gaussian(output_dim=OUTPUT_DIM,
                                   initial_variance=1e-3)

        self.layers = [self.layer1, self.layer2, self.layer3]
        self.data_size = data_size

    def forward(self, x):
        h1 = self.mean_func1(x) + self.layer1(x)
        h2 = self.mean_func2(h1) + self.layer2(h1)

        return self.layer3.pred_mean_and_var(h2)

    def calc_bound(self, y, f_dstr):
        pe = self.likelihood.predictive_expectation(y, f_dstr)

        kld = sum(layer.kl_divergence() for layer in self.layers)

        batch_size = y.size(0)
        bound = pe  - batch_size / self.data_size * kld

        return bound

    def predict(self, x):
        f_dstr = self.forward(x)

        y_dstr = self.likelihood.predict(f_dstr)
        return y_dstr

def make_dgp3():
    lengthscale = calc_median_of_pairwise_distance([dx])
    centroid = calc_centroid(NUM_INDUCINGS, [dx])

    model = DGP3(initial_inducing_input=centroid,
                 initial_lengthscale=lengthscale,
                 data_size=DATA_SIZE)

    return model

model = make_dgp3()
model

```

    DGP3(
      (layer1): SVGPLayer(
        input_dim=1, output_dim=3, num_inducings=20
        (kernel): RBF()
      )
      (layer2): SVGPLayer(
        input_dim=3, output_dim=3, num_inducings=20
        (kernel): RBF()
      )
      (layer3): SVGPLayer(
        input_dim=3, output_dim=1, num_inducings=20
        (kernel): RBF()
      )
      (likelihood): Gaussian()
    )

```python
optimizer = torch.optim.Adam(model.parameters(), lr=GP_LEARNING_RATE)
lower_bound_list, pred_mean_list = train(optimizer, model, num_epochs=DGP_NUM_EPOCHS)
```

    FloatProgress(value=0.0, max=1000.0)

## Results

```python
# plotting the change of ELBO
plt.plot(lower_bound_list)
```

### plot predicted mean for each epoch

```python
plt.plot(nx, pred_mean_list[0], label='init')
for j in [10, 100, 1000]:
    if j < len(pred_mean_list):
        plt.plot(nx, pred_mean_list[j], label=str(j))
plt.plot(dx, dy, 'o', label='data')
plt.legend()
```

## plot layer-wise value

```python
tx = torch.from_numpy(nx.astype(np.float32))
m1 = model.mean_func1(tx) + model.layer1.pred_mean_and_var(tx)['mean']
plt.plot(nx, m1.data.numpy())
```

```python
m2 = model.mean_func2(m1) + model.layer2.pred_mean_and_var(m1)['mean']
plt.plot(nx, m2.data.numpy())
```

Plot predictive distribution approximated by
$$
\begin{align}
p(y) &= \int p(y | f) q(f | \bar{h}^{(2)}) df \\
\bar{h}^{(2)} &= \mathbb{E}\left[ q(h^{(2)} | \bar{h}^{(1)}) \right] \\
\bar{h}^{(1)} &= \mathbb{E}\left[ q(h^{(1)} | x) \right].
\end{align}
$$

```python
f_dstr =model.layer3.pred_mean_and_var(m2)
y_dstr = model.likelihood.predict(f_dstr)
m_y = y_dstr['mean'].data.numpy()
s_y = np.sqrt(y_dstr['var'].data.numpy())
plt.fill_between(nx.ravel(), (m_y + s_y).ravel(), (m_y - s_y).ravel(), color='#cccccc')
plt.plot(nx, m_y, label='mean')
plt.plot(dx, dy, 'o', label='data')
plt.legend()
```

```python

```


ST0256 - Análisis Numérico

Capítulo 1: Error y Representación de números en el computador

2021/01

MEDELLÍN - COLOMBIA

 Text provided under a Creative Commons Attribution license, CC-BY. All code is made available under the FSF-approved MIT license.(c) Carlos Alberto Alvarez Henao

***

***Docente:*** Carlos Alberto Álvarez Henao, I.C. D.Sc.

***e-mail:*** calvar52@eafit.edu.co

***skype:*** carlos.alberto.alvarez.henao

***Herramienta:*** [Jupyter](http://jupyter.org/)

***Kernel:*** Python 3.8

***

Table of Contents

1  Fuentes de error

1.1  Error en el modelo y los datos

1.2  Error de truncamiento

1.3  Error de  representación de punto fotante

1.4  Definiciones básicas

2  Notación $\text{Big}-\mathcal{O}$

2.1  Reglas para el error de propagación basado en la notación $\text{Big}-\mathcal{O}$

3  Error de truncamiento

3.1  Laboratorio Numérico 1

4  Error de punto flotante

4.1  Aritmética de punto flotante

4.2  Propiedades de los sistemas de punto flotante

4.3  Sistema Binario

4.3.1  Numero y distribución de números

4.4  Sistema real - IEEE 754 sistema binario de punto flotante

4.4.1  Precisión simple

4.4.2  Precisión doble

4.5  Acceso de Python a números de la IEEE

4.6  Calculo "manual" del $\epsilon_{mach}$

5  Por qué debería importarnos esto?

5.1  Ejemplo 1: Aritmética simple

5.2  Ejemplo 2: Cancelación catastrófica

5.3  Ejemplo 3: Evaluación de una función

5.4  Ejemplo 4: Evaluación de un Polinomio

5.5  Ejemplo 5: Evaluación de una función racional

6  Combinación de error

6.1  Ejemplo 1:

6.2  Ejemplo 2:

6.3  Ejemplo 3: Error relativo

7  Operaciones de conteo

7.1  Ejemplo 1: Multiplicación matriz - vector

7.2  Ejemplo 2: Método de Horner para evaluar polinomios

7.3  Algoritmo

***Comentario:*** este capítulo está basado en parte de las  notas del curso del profesor [Kyle T. Mandli](https://github.com/mandli/intro-numerical-methods) (en inglés)

 Fuente: Ariane 5 Explosion

```python
#Bibliotecas a ser utilizadas en el Notebook

import numpy as np
import matplotlib.pyplot as plt
import sympy
import scipy.special

```

## Fuentes de error

Los cálculos numéricos, que involucran el uso de máquinas (análogas o digitales) presentan una serie de errores que provienen de diferentes fuentes:

- del Modelo

- de los datos

- de truncamiento

- de representación de los números (punto flotante)

- $ \ldots$

***Meta:*** Categorizar y entender cada tipo de error y explorar algunas aproximaciones simples para analizarlas.

[Volver a la Tabla de Contenido](#TOC)

### Error en el modelo y los datos

Errores en la formulación fundamental

- Error en los datos: imprecisiones en las mediciones o incertezas en los parámetros

Infortunadamente no tenemos control de los errores en los datos y el modelo de forma directa pero podemos usar métodos que pueden ser más robustos en la presencia de estos tipos de errores.

[Volver a la Tabla de Contenido](#TOC)

### Error de truncamiento

Los errores surgen de la expansión de funciones con una función simple, por ejemplo, $sin(x) \approx x$ para $|x|\approx0$.

[Volver a la Tabla de Contenido](#TOC)

### Error de  representación de punto fotante

Los errores surgen de aproximar números reales con la representación  en precisión finita de números en el computador.

[Volver a la Tabla de Contenido](#TOC)

### Definiciones básicas

Dado un valor verdadero de una función $f$ y una solución aproximada $F$, se define:

- ***Error absoluto***

$$e_a=|f-F|$$

***Ejemplo:*** se realiza una medición y se obtiene un valor aproximado de $29.99$ *m*. Asumiendo que el valor exacto de dicha medición debería ser de $30.00$ *m*, ¿cuál es el error absoluto obtenido? Cuál sería el error absoluto si se disminuye un orden de magnitud las cantidades?

```python
f = 3.00
F = 2.99
```

```python
ea = abs(f - F)
print("{0:6.4f}".format(ea*100))
```

```python
# reduciendo un órden de magnitud las cantidades

f = 30.00
F = 29.99
```

```python
ea = abs(f - F)
print("{0:6.4f}".format(ea*100))
```

Se observa que el valor del error absoluto es igual ($\approx 1.0$), independiente de la magnitud de las cantidades.

- ***Error relativo***

$$e_r (\%)= \frac{e_a}{|f|}=\frac{|f-F|}{|f|} \times 100 \%$$

***Ejemplo:*** Repetir el ejemplo anterior, pero calculando el error relativo porcentual.

```python
f = 3.00
F = 2.99
```

```python
er = abs(f - F) / f * 100
print("{0:6.4f}%".format(er))
```

```python
# reduciendo un órden de magnitud las cantidades

f = 30.00
F = 29.99
```

```python
er = abs(f - F) / f * 100
print("{0:6.4f}%".format(er))
```

Se observa que los resultados son diferentes y es mayor cuando las cantidades medidas son menores.

Entre las dos formas de representar el error, la relativa es más consistente con la magnitud de lo que se está midiendo.

[Volver a la Tabla de Contenido](#TOC)

## Notación $\text{Big}-\mathcal{O}$

sea $$f(x)= \mathcal{O}(g(x)) \text{ cuando } x \rightarrow a$$

si y solo si

$$|f(x)|\leq M|g(x)| \text{ cuando } |x-a| < \delta \text{ donde } M, a > 0$$

En la práctica, usamos la notación $\text{Big}-\mathcal{O}$ para decir algo sobre cómo se pueden comportar los términos que podemos haber dejado fuera de una serie. Veamos el siguiente ejemplo de la aproximación de la serie de Taylor:

***Ejemplo:***

sea $f(x) = \sin(x)$ con $x_0 = 0$ entonces

$$T_N(x) = \sum^N_{n=0} (-1)^{n} \frac{x^{2n+1}}{(2n+1)!}$$

Podemos escribir $f(x)$ como

$$f(x) = x - \frac{x^3}{6} + \frac{x^5}{120} + \mathcal{O}(x^7)$$

Esto se vuelve más útil cuando lo vemos como lo hicimos antes con $\Delta x$:

$$f(x) = \Delta x - \frac{\Delta x^3}{6} + \frac{\Delta x^5}{120} + \mathcal{O}(\Delta x^7)$$

[Volver a la Tabla de Contenido](#TOC)

### Reglas para el error de propagación basado en la notación $\text{Big}-\mathcal{O}$

En general, existen dos teoremas que no necesitan prueba y se mantienen cuando el valor de $x$ es grande:

Sea

$$\begin{aligned}
    f(x) &= p(x) + \mathcal{O}(x^n) \\
    g(x) &= q(x) + \mathcal{O}(x^m) \\
    k &= \max(n, m)
\end{aligned}$$

Entonces

$$
    f+g = p + q + \mathcal{O}(x^k)
$$

y

\begin{align}
    f \cdot g &= p \cdot q + p \mathcal{O}(x^m) + q \mathcal{O}(x^n) + O(x^{n + m}) \\
    &= p \cdot q + \mathcal{O}(x^{n+m})
\end{align}

De otra forma, si estamos interesados en valores pequeños de $x$, $\Delta x$, la expresión puede ser modificada como sigue:

\begin{align}
    f(\Delta x) &= p(\Delta x) + \mathcal{O}(\Delta x^n) \\
    g(\Delta x) &= q(\Delta x) + \mathcal{O}(\Delta x^m) \\
    r &= \min(n, m)
\end{align}

entonces

$$
    f+g = p + q + O(\Delta x^r)
$$

y

\begin{align}
    f \cdot g &= p \cdot q + p \cdot \mathcal{O}(\Delta x^m) + q \cdot \mathcal{O}(\Delta x^n) + \mathcal{O}(\Delta x^{n+m}) \\
    &= p \cdot q + \mathcal{O}(\Delta x^r)
\end{align}

***Nota:*** En este caso, supongamos que al menos el polinomio con $k=max(n,m)$ tiene la siguiente forma:

$$
    p(\Delta x) = 1 + p_1 \Delta x + p_2 \Delta x^2 + \ldots
$$

o

$$
    q(\Delta x) = 1 + q_1 \Delta x + q_2 \Delta x^2 + \ldots
$$

para que $\mathcal{O}(1)$

de modo que hay un término $\mathcal{O}(1)$ que garantiza la existencia de $\mathcal{O}(\Delta x^r)$ en el producto final.

Para tener una idea de por qué importa más la potencia en $\Delta x$ al considerar la convergencia, la siguiente figura muestra cómo las diferentes potencias en la tasa de convergencia pueden afectar la rapidez con la que converge nuestra solución. Tenga en cuenta que aquí estamos dibujando los mismos datos de dos maneras diferentes. Graficar el error como una función de $\Delta x$ es una forma común de mostrar que un método numérico está haciendo lo que esperamos y muestra el comportamiento de convergencia correcto. Dado que los errores pueden reducirse rápidamente, es muy común trazar este tipo de gráficos en una escala log-log para visualizar fácilmente los resultados. Tenga en cuenta que si un método fuera realmente del orden $n$, será una función lineal en el espacio log-log con pendiente $n$.

```python
dx = np.linspace(1.0, 1e-4, 100)

fig = plt.figure()
fig.set_figwidth(fig.get_figwidth() * 2.0)
axes = []
axes.append(fig.add_subplot(1, 2, 1))
axes.append(fig.add_subplot(1, 2, 2))

for n in range(1, 5):
    axes[0].plot(dx, dx**n, label="$\Delta x^%s$" % n)
    axes[1].loglog(dx, dx**n, label="$\Delta x^%s$" % n)

axes[0].legend(loc=2)
axes[1].set_xticks([10.0**(-n) for n in range(5)])
axes[1].set_yticks([10.0**(-n) for n in range(16)])
axes[1].legend(loc=4)
for n in range(2):
    axes[n].set_title("Crecimiento del Error vs. $\Delta x^n$")
    axes[n].set_xlabel("$\Delta x$")
    axes[n].set_ylabel("Error Estimado")
    axes[n].set_title("Crecimiento de las diferencias")
    axes[n].set_xlabel("$\Delta x$")
    axes[n].set_ylabel("Error Estimado")

plt.show()
```

[Volver a la Tabla de Contenido](#TOC)

## Error de truncamiento

***Teorema de Taylor:*** Sea $f(x) \in C^{m+1}[a,b]$ y $x_0 \in [a,b]$, para todo $x \in (a,b)$ existe un número $c = c(x)$ que se encuentra entre $x_0$ y $x$ tal que

$$ f(x) = T_N(x) + R_N(x)$$

donde $T_N(x)$ es la aproximación del polinomio de Taylor

$$T_N(x) = \sum^N_{n=0} \frac{f^{(n)}(x_0)\times(x-x_0)^n}{n!}$$

y $R_N(x)$ es el residuo (la parte de la serie que obviamos)

$$R_N(x) = \frac{f^{(n+1)}(c) \times (x - x_0)^{n+1}}{(n+1)!}$$

Otra forma de pensar acerca de estos resultados consiste en reemplazar $x - x_0$ con $\Delta x$. La idea principal es que el residuo $R_N(x)$ se vuelve mas pequeño cuando $\Delta x \rightarrow 0$.

$$T_N(x) = \sum^N_{n=0} \frac{f^{(n)}(x_0)\times \Delta x^n}{n!}$$

y $R_N(x)$ es el residuo (la parte de la serie que obviamos)

$$ R_N(x) = \frac{f^{(n+1)}(c) \times \Delta x^{n+1}}{(n+1)!} \leq M \Delta x^{n+1}$$

***Ejemplo 1:***

$f(x) = e^x$ con $x_0 = 0$

Usando esto podemos encontrar expresiones para el error relativo y absoluto en función de $x$ asumiendo $N=2$.

Derivadas:
$$\begin{aligned}
    f'(x) &= e^x \\
    f''(x) &= e^x \\
    f^{(n)}(x) &= e^x
\end{aligned}$$

Polinomio de Taylor:
$$\begin{aligned}
    T_N(x) &= \sum^N_{n=0} e^0 \frac{x^n}{n!} \Rightarrow \\
    T_2(x) &= 1 + x + \frac{x^2}{2}
\end{aligned}$$

Restos:
$$\begin{aligned}
    R_N(x) &= e^c \frac{x^{n+1}}{(n+1)!} = e^c \times \frac{x^3}{6} \quad \Rightarrow \\
    R_2(x) &\leq \frac{e^1}{6} \approx 0.5
\end{aligned}$$

Precisión:
$$
    e^1 = 2.718\ldots \\
    T_2(1) = 2.5 \Rightarrow e \approx 0.2 ~~ r \approx 0.1
$$

¡También podemos usar el paquete `sympy` que tiene la capacidad de calcular el polinomio de *Taylor* integrado!

```python
x = sympy.symbols('x')
f = sympy.symbols('f', cls=sympy.Function)

f = sympy.exp(x)
f.series(x0=0, n=11)
```

```python
a = 1.1**500

print(a)
```

Graficando

```python
x = np.linspace(-5, 5, 100)
T_N = 1.0 + x + x**2 / 2.0 + x**3 / 6.0 + x**4 / 24.0 + x**5 / 120.0 + x**6 / 720.0 + x**7 / 5040.0 + x**8 / 40320.0 + x**9 / 362880
R_N = np.exp(1) * x**10 / 3628800.0

plt.plot(x, T_N, 'r', x, np.exp(x), 'k', x, R_N, 'b')
plt.plot(0.0, 1.0, 'o', markersize=10)
plt.grid(True)
plt.xlabel("x")
plt.ylabel("$f(x)$, $T_N(x)$, $R_N(x)$")
plt.legend(["$T_N(x)$", "$f(x)$", "$R_N(x)$"], loc=2)
plt.show()
```

```python
R_N
```

***Ejemplo 2:***

Aproximar

$$ f(x) = \frac{1}{x} \quad x_0  = 1,$$

usando $x_0 = 1$ para el tercer termino de la serie de Taylor.

$$\begin{aligned}
    f'(x) &= -\frac{1}{x^2} \\
    f''(x) &= \frac{2}{x^3} \\
    f^{(n)}(x) &= \frac{(-1)^n n!}{x^{n+1}}
\end{aligned}$$

$$\begin{aligned}
    T_N(x) &= \sum^N_{n=0} (-1)^n (x-1)^n \Rightarrow \\
    T_2(x) &= 1 - (x - 1) + (x - 1)^2
\end{aligned}$$

$$\begin{aligned}
    R_N(x) &= \frac{(-1)^{n+1}(x - 1)^{n+1}}{c^{n+2}} \Rightarrow \\
    R_2(x) &= \frac{-(x - 1)^{3}}{c^{4}}
\end{aligned}$$

```python
x = np.linspace(0.8, 2, 100)
T_N = 1.0 - (x-1) + (x-1)**2
R_N = -(x-1.0)**3 / (1.1**4)

plt.plot(x, T_N, 'r', x, 1.0 / x, 'k', x, R_N, 'b')
plt.plot(1.0, 1.0, 'o', markersize=10)
plt.grid(True)
plt.xlabel("x")
plt.ylabel("$f(x)$, $T_N(x)$, $R_N(x)$")

plt.legend(["$T_N(x)$", "$f(x)$", "$R_N(x)$"], loc=8)
plt.show()
```

[Volver a la Tabla de Contenido](#TOC)

### Laboratorio Numérico 1

$\color{red}{\textbf{Ejercicio:}}$ Realice la expansión de la serie de Taylor para los dos ejemplos anteriores con 3, 4 y 5 términos. Cuál es el error que se tiene a medida que se adicionan más términos? Realice una gráfica comparativa del Residuo que se obtiene para cada término adicional. Haga un análisis de lo que sucede. Si extendemos hasta el "infinito" dicho residuo qué pueden concluir?

[Volver a la Tabla de Contenido](#TOC)

## Error de punto flotante

Errores surgen de aproximar números reales con números de precisión finita

$$\pi \approx 3.14$$

o $\frac{1}{3} \approx 0.333333333$ en decimal, los resultados forman un número finito de registros para representar cada número.

***Ej.:*** considere la representación de $\sqrt{2}=1.4142 \ldots$. Como sabemos, éste es un número irracional, es decir, tiene una cantidad infinita de dígitos decimales. El computador almacena de forma incompleta la represenación de ese valor empleando cierta cantidad de números decimales

$$2 - (\sqrt{2})^2$$

```python
a = np.sqrt(2)
print("a: ", a)

b = abs(2-a**2)
print("|2-a^2| = ", b)

```

[Volver a la Tabla de Contenido](#TOC)

### Aritmética de punto flotante

Los números en sistemas de [punto flotante](https://en.wikipedia.org/wiki/Floating-point_arithmetic "Floating point arithmetic") se representan como una serie de bits que representan diferentes partes de un número. En los sistemas de punto flotante normalizados, existen algunas convenciones estándar para el uso de estos bits. En general, los números se almacenan dividiéndolos en la forma

$$F = \pm d_1 . d_2 d_3 d_4 \ldots d_p \times \beta^E$$

donde

1. $\pm$ es un bit único y representa el signo del número.

2. $d_1 . d_2 d_3 d_4 \ldots d_p$ es la *mantisa*. observe que, técnicamente, el decimal se puede mover, pero en general, utilizando la notación científica, el decimal siempre se puede colocar en esta ubicación. Los digitos $d_2 d_3 d_4 \ldots d_p$ son llamados la *fracción* con $p$ digitos de precisión. Los sistemas normalizados específicamente ponen el punto decimal en el frente y asume $d_1 \neq 0$ a menos que el número sea exactamente $0$.

3. $\beta$ es la *base*. Para el sistema binario $\beta = 2$, para decimal $\beta = 10$, etc.

4. $E$ es el *exponente*, un entero en el rango $[E_{\min}, E_{\max}]$

Los puntos importantes en cualquier sistema de punto flotante son:

1. Existe un conjunto discreto y finito de números representables.

2. Estos números representables no están distribuidos uniformemente en la línea real

3. La aritmética en sistemas de punto flotante produce resultados diferentes de la aritmética de precisión infinita (es decir, matemática "real")

[Volver a la Tabla de Contenido](#TOC)

### Propiedades de los sistemas de punto flotante

Todos los sistemas de punto flotante se caracterizan por varios números importantes

- Número normalizado reducido (underflow si está por debajo, relacionado con números sub-normales alrededor de cero)

- Número normalizado más grande (overflow)

- Cero

- $\epsilon$ o $\epsilon_{mach}$

- `Inf` y `nan`

***Ejemplo: Sistema de juguete***

Considere el sistema decimal de 2 digitos de  precisión (normalizado)

$$f = \pm d_1 . d_2 \times 10^E$$

con $E \in [-2, 0]$.

**Numero y distribución de números**

1. Cuántos números pueden representarse con este sistema?

2. Cuál es la distribución en la línea real?

3. Cuáles son los límites underflow y overflow?

Cuántos números pueden representarse con este sistema?

$$f = \pm d_1 . d_2 \times 10^E ~~~ \text{with} E \in [-2, 0]$$

$$2 \times 9 \times 10 \times 3 + 1 = 541$$

Cuál es la distribución en la línea real?

```python
d_1_values = [1, 2, 3, 4, 5, 6, 7, 8, 9]
d_2_values = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
E_values = [0, -1, -2]

fig = plt.figure(figsize=(10.0, 1.0))
axes = fig.add_subplot(1, 1, 1)

for E in E_values:
    for d1 in d_1_values:
        for d2 in d_2_values:
            axes.plot( (d1 + d2 * 0.1) * 10**E, 0.0, 'r+', markersize=20)
            axes.plot(-(d1 + d2 * 0.1) * 10**E, 0.0, 'r+', markersize=20)

axes.plot(0.0, 0.0, '+', markersize=20)
axes.plot([-10.0, 10.0], [0.0, 0.0], 'k')

axes.set_title("Distribución de Valores")
axes.set_yticks([])
axes.set_xlabel("x")
axes.set_ylabel("")
axes.set_xlim([-0.1, 0.1])
plt.show()
```

Cuáles son los límites superior (overflow) e inferior (underflow)?

- El menor número que puede ser representado (underflow) es: $1.0 \times 10^{-2} = 0.01$

- El mayor número que puede ser representado (overflow) es:  $9.9 \times 10^0 = 9.9$

[Volver a la Tabla de Contenido](#TOC)

### Sistema Binario

Considere el sistema en base 2 de 2 dígitos de precisión

$$f=\pm d_1 . d_2 \times 2^E \quad \text{with} \quad E \in [-1, 1]$$

[Volver a la Tabla de Contenido](#TOC)

#### Numero y distribución de números

1. Cuántos números pueden representarse con este sistema?

2. Cuál es la distribución en la línea real?

3. Cuáles son los límites underflow y overflow?

Cuántos números pueden representarse en este sistema?

$$f=\pm d_1 . d_2 \times 2^E ~~~~ \text{con} ~~~~ E \in [-1, 1]$$

$$ 2 \times 1 \times 2 \times 3 + 1 = 13$$

Cuál es la distribución en la línea real?

```python
d_1_values = [1]
d_2_values = [0, 1]
E_values = [1, 0, -1]

fig = plt.figure(figsize=(10.0, 1.0))
axes = fig.add_subplot(1, 1, 1)

for E in E_values:
    for d1 in d_1_values:
        for d2 in d_2_values:
            axes.plot( (d1 + d2 * 0.5) * 2**E, 0.0, 'r+', markersize=20)
            axes.plot(-(d1 + d2 * 0.5) * 2**E, 0.0, 'r+', markersize=20)

axes.plot(0.0, 0.0, 'r+', markersize=20)
axes.plot([-4.5, 4.5], [0.0, 0.0], 'k')

axes.set_title("Distribución de Valores")
axes.set_yticks([])
axes.set_xlabel("x")
axes.set_ylabel("")
axes.set_xlim([-3.5, 3.5])
plt.show()
```

Cuáles son los límites superior (*overflow*) e inferior (*underflow*)?

- El menor número que puede ser representado (*underflow*) es: $1.0 \times 2^{-1} = 0.5$

- El mayor número que puede ser representado (*overflow*) es:  $1.1 \times 2^1 = 3$

Observe que estos números son en sistema binario.

Una rápida regla de oro:

$$2^3 2^2 2^1 2^0 . 2^{-1} 2^{-2} 2^{-3}$$

corresponde a

8s, 4s, 2s, 1s . mitades, cuartos, octavos, $\ldots$

[Volver a la Tabla de Contenido](#TOC)

### Sistema real - [IEEE 754](https://en.wikipedia.org/wiki/IEEE_754) sistema binario de punto flotante

#### Precisión simple

- Almacenamiento total es de 32 bits

- Exponente de 8 bits $\Rightarrow E \in [-126, 127]$

- Fracción 23 bits ($p = 24$)

```
s EEEEEEEE FFFFFFFFFFFFFFFFFFFFFFF
0 1      8 9                     31
```

Overflow $= 2^{127} \approx 3.4 \times 10^{38}$

Underflow $= 2^{-126} \approx 1.2 \times 10^{-38}$

$\epsilon_{\text{machine}} = 2^{-23} \approx 1.2 \times 10^{-7}$

[Volver a la Tabla de Contenido](#TOC)

#### Precisión doble

- Almacenamiento total asignado es 64 bits

- Exponenete de 11 bits $\Rightarrow E \in [-1022, 1024]$

- Fracción de 52 bits ($p = 53$)

```
s EEEEEEEEEE FFFFFFFFFF FFFFFFFFFF FFFFFFFFFF FFFFFFFFFF FFFFFFFFFF FF
0 1       11 12                                                      63
```
Overflow $= 2^{1024} \approx 1.8 \times 10^{308}$

Underflow $= 2^{-1022} \approx 2.2 \times 10^{-308}$

$\epsilon_{\text{machine}} = 2^{-52} \approx 2.2 \times 10^{-16}$

[Volver a la Tabla de Contenido](#TOC)

### Acceso de Python a números de la IEEE

Accede a muchos parámetros importantes, como el epsilon de la máquina

```python
import numpy as np
np.finfo(float).eps
```

```python
np.finfo(float).eps

print(np.finfo(np.float16))
print(np.finfo(np.float32))
print(np.finfo(float))
```

[Volver a la Tabla de Contenido](#TOC)

### Calculo "manual" del $\epsilon_{mach}$

La determinación "manual" del $\epsilon_{mach}$ es muy simple. Veamos el siguiente algoritmo

```python
eps = 1.0

while 1.0 + eps > 1.0:
    eps = eps / 2.0

print(eps)
```

Si lo comparamos con el valor obtenido en el numeral anterior para `float64`, `2.2204460492503131e-16`, se observa que es del orden de dos veces menor, por qué?

[Volver a la Tabla de Contenido](#TOC)

## Por qué debería importarnos esto?

- Aritmética de punto flotante no es conmutativa o asociativa

- Errores de punto flotante compuestos, No asuma que la precisión doble es suficiente

- Mezclar precisión es muy peligroso

***EL ORDEN DE LOS FACTORES NO ALTERA EL PRODUCTO!!!***

$$2 \times 3 = 3 \times 2 = 6$$

$$ 10^{300} \times 10^{50} \times 10^{-60} = 10^{300} \times 10^{-60} \times 10^{50} ??$$

```python
a = 10**300
b = 10**10
c = 10**-60

```

```python
d1 = a * b * c
print("d1: ", d1)
```

```python
d2 = b * c * a
print("d2: ", d2)

```

[Volver a la Tabla de Contenido](#TOC)

### Ejemplo 1: Aritmética simple

Aritmética simple $\delta < \epsilon_{\text{machine}}$

   $$(1+\delta) - 1 = 1 - 1 = 0$$

   $$1 - 1 + \delta = \delta$$

```python
delta = 1.0000000001 * eps

value = (1 + delta) - 1
print(value)
```

[Volver a la Tabla de Contenido](#TOC)

### Ejemplo 2: Cancelación catastrófica

Miremos qué sucede cuando sumamos dos números $x$ y $y$ cuando $x+y \neq 0$. De hecho, podemos estimar estos límites haciendo un análisis de error. Aquí necesitamos presentar la idea de que cada operación de punto flotante introduce un error tal que

$$
    \text{fl}(x ~\text{op}~ y) = (x ~\text{op}~ y) (1 + \delta)
$$

donde $\text{fl}(\cdot)$ es una función que devuelve la representación de punto flotante de la expresión encerrada, $\text{op}$ es alguna operación (ex. $+, -, \times, /$), y $\delta$ es el error de punto flotante debido a  $\text{op}$.

De vuelta a nuestro problema en cuestión. El error de coma flotante debido a la suma es

$$\text{fl}(x + y) = (x + y) (1 + \delta).$$

Comparando esto con la solución verdadera usando un error relativo tenemos

$$\begin{aligned}
    \frac{(x + y) - \text{fl}(x + y)}{x + y} &= \frac{(x + y) - (x + y) (1 + \delta)}{x + y} = \delta.
\end{aligned}$$

entonces si $\delta = \mathcal{O}(\epsilon_{\text{machine}})$ no estaremos muy preocupados.

Que pasa si consideramos un error de punto flotante en la representación de $x$ y $y$, $x \neq y$, y decimos que $\delta_x$ y $\delta_y$ son la magnitud de los errores en su representación. Asumiremos que esto constituye el error de punto flotante en lugar de estar asociado con la operación en sí.

Dado todo esto, tendríamos

$$\begin{aligned}
    \text{fl}(x + y) &= x (1 + \delta_x) + y (1 + \delta_y) \\
    &= x + y + x \delta_x + y \delta_y \\
    &= (x + y) \left(1 + \frac{x \delta_x + y \delta_y}{x + y}\right)
\end{aligned}$$

Calculando nuevamente el error relativo, tendremos

$$\begin{aligned}
    \frac{x + y - (x + y) \left(1 + \frac{x \delta_x + y \delta_y}{x + y}\right)}{x + y} &= 1 - \left(1 + \frac{x \delta_x + y \delta_y}{x + y}\right) \\
    &= \frac{x}{x + y} \delta_x + \frac{y}{x + y} \delta_y \\
    &= \frac{1}{x + y} (x \delta_x + y \delta_y)
\end{aligned}$$

Lo importante aquí es que ahora el error depende de los valores de $x$ y $y$, y más importante aún, su suma. De particular preocupación es el tamaño relativo de $x + y$. A medida que se acerca a cero en relación con las magnitudes de $x$ y $y$, el error podría ser arbitrariamente grande. Esto se conoce como ***cancelación catastrófica***.

```python
dx = np.array([10**(-n) for n in range(1, 16)])
x = 1.0 + dx
y = -np.ones(x.shape)
error = np.abs(x + y - dx) / (dx)

fig = plt.figure()
fig.set_figwidth(fig.get_figwidth() * 2)

axes = fig.add_subplot(1, 2, 1)
axes.loglog(dx, x + y, 'o-')
axes.set_xlabel("$\Delta x$")
axes.set_ylabel("$x + y$")
axes.set_title("$\Delta x$ vs. $x+y$")

axes = fig.add_subplot(1, 2, 2)
axes.loglog(dx, error, 'o-')
axes.set_xlabel("$\Delta x$")
axes.set_ylabel("$|x + y - \Delta x| / \Delta x$")
axes.set_title("Diferencia entre $x$ y $y$ vs. Error relativo")

plt.show()
```

[Volver a la Tabla de Contenido](#TOC)

### Ejemplo 3: Evaluación de una función

Considere la función

$$
    f(x) = \frac{1 - \cos x}{x^2}
$$

con $x\in[-10^{-4}, 10^{-4}]$.

Tomando el límite cuando $x \rightarrow 0$ podemos ver qué comportamiento esperaríamos ver al evaluar esta función:

$$
    \lim_{x \rightarrow 0} \frac{1 - \cos x}{x^2} = \lim_{x \rightarrow 0} \frac{\sin x}{2 x} = \lim_{x \rightarrow 0} \frac{\cos x}{2} = \frac{1}{2}.
$$

¿Qué hace la representación de punto flotante?

```python
x = np.linspace(-1e-3, 1e-3, 100, dtype=np.float32)
error = (0.5 - (1.0 - np.cos(x)) / x**2) / 0.5

fig = plt.figure()
axes = fig.add_subplot(1, 1, 1)
axes.plot(x, error, 'o')
axes.set_xlabel("x")
axes.set_ylabel("Error Relativo")
```

[Volver a la Tabla de Contenido](#TOC)

### Ejemplo 4: Evaluación de un Polinomio

 $$f(x) = x^7 - 7x^6 + 21 x^5 - 35 x^4 + 35x^3-21x^2 + 7x - 1$$

```python
x = np.linspace(0.988, 1.012, 1000, dtype=np.float16)
y = x**7 - 7.0 * x**6 + 21.0 * x**5 - 35.0 * x**4 + 35.0 * x**3 - 21.0 * x**2 + 7.0 * x - 1.0

fig = plt.figure()
axes = fig.add_subplot(1, 1, 1)
axes.plot(x, y, 'r')
axes.set_xlabel("x")
axes.set_ylabel("y")
axes.set_ylim((-0.1, 0.1))
axes.set_xlim((x[0], x[-1]))
plt.show()
```

[Volver a la Tabla de Contenido](#TOC)

### Ejemplo 5: Evaluación de una función racional

Calcule $f(x) = x + 1$ por la función $$F(x) = \frac{x^2 - 1}{x - 1}$$

¿Cuál comportamiento esperarías encontrar?

```python
x = np.linspace(0.5, 1.5, 101, dtype=numpy.float16)
f_hat = (x**2 - 1.0) / (x - 1.0)

fig = plt.figure()
axes = fig.add_subplot(1, 1, 1)
axes.plot(x, numpy.abs(f_hat - (x + 1.0)))
axes.set_xlabel("$x$")
axes.set_ylabel("Error Absoluto")
plt.show()
```

[Volver a la Tabla de Contenido](#TOC)

## Combinación de error

En general, nos debemos ocupar de la combinación de error de truncamiento con el error de punto flotante.

- Error de Truncamiento: errores que surgen de la aproximación de una función, truncamiento de una serie.

$$\sin x \approx x - \frac{x^3}{3!} + \frac{x^5}{5!} + O(x^7)$$

- Error de punto flotante: errores derivados de la aproximación de números reales con números de precisión finita

$$\pi \approx 3.14$$

o $\frac{1}{3} \approx 0.333333333$ en decimal, los resultados forman un número finito de registros para representar cada número.

[Volver a la Tabla de Contenido](#TOC)

### Ejemplo 1:

Considere la aproximación de diferencias finitas donde $f(x) = e^x$ y estamos evaluando en $x=1$

$$f'(x) \approx \frac{f(x + \Delta x) - f(x)}{\Delta x}$$

Compare el error entre disminuir $\Delta x$ y la verdadera solucion $f'(1) = e$

```python
delta_x = np.linspace(1e-20, 5.0, 100)
delta_x = np.array([2.0**(-n) for n in range(1, 60)])
x = 1.0
f_hat_1 = (np.exp(x + delta_x) - np.exp(x)) / (delta_x)
f_hat_2 = (np.exp(x + delta_x) - np.exp(x - delta_x)) / (2.0 * delta_x)

fig = plt.figure()
axes = fig.add_subplot(1, 1, 1)
axes.loglog(delta_x, np.abs(f_hat_1 - np.exp(1)), 'o-', label="Unilateral")
axes.loglog(delta_x, np.abs(f_hat_2 - np.exp(1)), 's-', label="Centrado")
axes.legend(loc=3)
axes.set_xlabel("$\Delta x$")
axes.set_ylabel("Error Absoluto")
plt.show()
```

[Volver a la Tabla de Contenido](#TOC)

### Ejemplo 2:

Evalúe $e^x$ con la serie de *Taylor*

$$e^x = \sum^\infty_{n=0} \frac{x^n}{n!}$$

podemos elegir $n< \infty$ que puede aproximarse $e^x$ en un rango dado $x \in [a,b]$ tal que el error relativo $E$ satisfaga $E<8 \cdot \varepsilon_{\text{machine}}$?

¿Cuál podría ser una mejor manera de simplemente evaluar el polinomio de Taylor directamente por varios $N$?

```python
def my_exp(x, N=10):
    value = 0.0
    for n in range(N + 1):
        value += x**n / scipy.special.factorial(n)

    return value

x = np.linspace(-2, 2, 100, dtype=np.float32)
for N in range(1, 50):
    error = np.abs((np.exp(x) - my_exp(x, N=N)) / np.exp(x))
    if np.all(error < 8.0 * np.finfo(float).eps):
        break

print(N)

fig = plt.figure()
axes = fig.add_subplot(1, 1, 1)
axes.plot(x, error)
axes.set_xlabel("x")
axes.set_ylabel("Error Relativo")
plt.show()
```

[Volver a la Tabla de Contenido](#TOC)

### Ejemplo 3: Error relativo

Digamos que queremos calcular el error relativo de dos valores $x$ y $y$ usando $x$ como valor de normalización

$$
    E = \frac{x - y}{x}
$$
y
$$
    E = 1 - \frac{y}{x}
$$

son equivalentes. En precisión finita, ¿qué forma pidría esperarse que sea más precisa y por qué?

Ejemplo tomado de [blog](https://nickhigham.wordpress.com/2017/08/14/how-and-how-not-to-compute-a-relative-error/) posteado por Nick Higham*

Usando este modelo, la definición original contiene dos operaciones de punto flotante de manera que

$$\begin{aligned}
    E_1 = \text{fl}\left(\frac{x - y}{x}\right) &= \text{fl}(\text{fl}(x - y) / x) \\
    &= \left[ \frac{(x - y) (1 + \delta_+)}{x} \right ] (1 + \delta_/) \\
    &= \frac{x - y}{x}  (1 + \delta_+) (1 + \delta_/)
\end{aligned}$$

Para la otra formulación tenemos

$$\begin{aligned}
    E_2 = \text{fl}\left( 1 - \frac{y}{x} \right ) &= \text{fl}\left(1 - \text{fl}\left(\frac{y}{x}\right) \right) \\
    &= \left(1 - \frac{y}{x} (1 + \delta_/) \right) (1 + \delta_-)
\end{aligned}$$

Si suponemos que todos las $\text{op}$s tienen magnitudes de error similares, entonces podemos simplificar las cosas dejando que

$$
    |\delta_\ast| \le \epsilon.
$$

Para comparar las dos formulaciones, nuevamente usamos el error relativo entre el error relativo verdadero $e_i$ y nuestras versiones calculadas $E_i$

Definición original

$$\begin{aligned}
    \frac{e - E_1}{e} &= \frac{\frac{x - y}{x} - \frac{x - y}{x}  (1 + \delta_+) (1 + \delta_/)}{\frac{x - y}{x}} \\
    &\le 1 - (1 + \epsilon) (1 + \epsilon) = 2 \epsilon + \epsilon^2
\end{aligned}$$

Definición manipulada:

$$\begin{aligned}
    \frac{e - E_2}{e} &= \frac{e - \left[1 - \frac{y}{x}(1 + \delta_/) \right] (1 + \delta_-)}{e} \\
    &= \frac{e - \left[e - \frac{y}{x} \delta_/) \right] (1 + \delta_-)}{e} \\
    &= \frac{e - \left[e + e\delta_- - \frac{y}{x} \delta_/ - \frac{y}{x} \delta_/ \delta_-)) \right] }{e} \\
    &= - \delta_- + \frac{1}{e} \frac{y}{x} \left(\delta_/ + \delta_/ \delta_- \right) \\
    &= - \delta_- + \frac{1 -e}{e} \left(\delta_/ + \delta_/ \delta_- \right) \\
    &\le \epsilon + \left |\frac{1 - e}{e}\right | (\epsilon + \epsilon^2)
\end{aligned}$$

Vemos entonces que nuestro error de punto flotante dependerá de la magnitud relativa de $e$

```python
# Based on the code by Nick Higham
# https://gist.github.com/higham/6f2ce1cdde0aae83697bca8577d22a6e
# Compares relative error formulations using single precision and compared to double precision

N = 501    # Note: Use 501 instead of 500 to avoid the zero value
d = numpy.finfo(numpy.float32).eps * 1e4
a = 3.0
x = a * numpy.ones(N, dtype=numpy.float32)
y = [x[i] + numpy.multiply((i - numpy.divide(N, 2.0, dtype=numpy.float32)), d, dtype=numpy.float32) for i in range(N)]

# Compute errors and "true" error
relative_error = numpy.empty((2, N), dtype=numpy.float32)
relative_error[0, :] = numpy.abs(x - y) / x
relative_error[1, :] = numpy.abs(1.0 - y / x)
exact = numpy.abs( (numpy.float64(x) - numpy.float64(y)) / numpy.float64(x))

# Compute differences between error calculations
error = numpy.empty((2, N))
for i in range(2):
    error[i, :] = numpy.abs((relative_error[i, :] - exact) / numpy.abs(exact))

fig = plt.figure()
axes = fig.add_subplot(1, 1, 1)
axes.semilogy(y, error[0, :], '.', markersize=10, label="$|x-y|/|x|$")
axes.semilogy(y, error[1, :], '.', markersize=10, label="$|1-y/x|$")

axes.grid(True)
axes.set_xlabel("y")
axes.set_ylabel("Error Relativo")
axes.set_xlim((numpy.min(y), numpy.max(y)))
axes.set_ylim((5e-9, numpy.max(error[1, :])))
axes.set_title("Comparasión Error Relativo")
axes.legend()
plt.show()
```

Algunos enlaces de utilidad con respecto al punto flotante IEEE:

- [What Every Computer Scientist Should Know About Floating-Point Arithmetic](http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html)

- [IEEE 754 Floating Point Calculator](http://babbage.cs.qc.edu/courses/cs341/IEEE-754.html)

- [Numerical Computing with IEEE Floating Point Arithmetic](http://epubs.siam.org/doi/book/10.1137/1.9780898718072)

[Volver a la Tabla de Contenido](#TOC)

## Operaciones de conteo

- ***Error de truncamiento:*** *¿Por qué no usar más términos en la serie de Taylor?*

- ***Error de punto flotante:*** *¿Por qué no utilizar la mayor precisión posible?*

[Volver a la Tabla de Contenido](#TOC)

### Ejemplo 1: Multiplicación matriz - vector

Sea $A, B \in \mathbb{R}^{N \times N}$ y $x \in \mathbb{R}^N$.

1. Cuenta el número aproximado de operaciones que tomará para calcular $Ax$

2. Hacer lo mismo para $AB$

***Producto Matriz-vector:*** Definiendo $[A]_i$ como la $i$-ésima fila de $A$ y $A_{ij}$ como la $i$,$j$-ésima entrada entonces

$$
    A x = \sum^N_{i=1} [A]_i \cdot x = \sum^N_{i=1} \sum^N_{j=1} A_{ij} x_j
$$

Tomando un caso en particular, siendo $N=3$, entonces la operación de conteo es

$$
    A x = [A]_1 \cdot v + [A]_2 \cdot v + [A]_3 \cdot v = \begin{bmatrix}
        A_{11} \times v_1 + A_{12} \times v_2 + A_{13} \times v_3 \\
        A_{21} \times v_1 + A_{22} \times v_2 + A_{23} \times v_3 \\
        A_{31} \times v_1 + A_{32} \times v_2 + A_{33} \times v_3
    \end{bmatrix}
$$

Esto son 15 operaciones (6 sumas y 9 multiplicaciones)

Tomando otro caso, siendo $N=4$, entonces el conteo de operaciones es:

$$
    A x = [A]_1 \cdot v + [A]_2 \cdot v + [A]_3 \cdot v = \begin{bmatrix}
        A_{11} \times v_1 + A_{12} \times v_2 + A_{13} \times v_3 + A_{14} \times v_4 \\
        A_{21} \times v_1 + A_{22} \times v_2 + A_{23} \times v_3 + A_{24} \times v_4 \\
        A_{31} \times v_1 + A_{32} \times v_2 + A_{33} \times v_3 + A_{34} \times v_4 \\
        A_{41} \times v_1 + A_{42} \times v_2 + A_{43} \times v_3 + A_{44} \times v_4 \\
    \end{bmatrix}
$$

Esto lleva a 28 operaciones (12 sumas y 16 multiplicaciones).

Generalizando, hay $N^2$ mutiplicaciones y $N(N-1)$ sumas para un total de

$$
    \text{operaciones} = N (N - 1) + N^2 = \mathcal{O}(N^2).
$$

***Producto Matriz-Matriz ($AB$):*** Definiendo $[B]_j$ como la $j$-ésima columna de $B$ entonces

$$
    (A B)_{ij} = \sum^N_{i=1} \sum^N_{j=1} [A]_i \cdot [B]_j
$$

El producto interno de dos vectores es representado por

$$
    a \cdot b = \sum^N_{i=1} a_i b_i
$$

conduce a $\mathcal{O}(3N)$ operaciones. Como hay $N^2$ entradas en la matriz resultante, tendríamos $\mathcal{O}(N^3)$ operaciones

Existen métodos para realizar la multiplicación matriz - matriz más rápido. En la siguiente figura vemos una colección de algoritmos a lo largo del tiempo que han podido limitar el número de operaciones en ciertas circunstancias
$$
    \mathcal{O}(N^\omega)
$$

[Volver a la Tabla de Contenido](#TOC)

### Ejemplo 2: Método de Horner para evaluar polinomios

Dado

$$P_N(x) = a_0 + a_1 x + a_2 x^2 + \ldots + a_N x^N$$

o

$$P_N(x) = p_1 x^N + p_2 x^{N-1} + p_3 x^{N-2} + \ldots + p_{N+1}$$

queremos encontrar la mejor vía para evaluar $P_N(x)$

Primero considere dos vías para escribir $P_3$

$$ P_3(x) = p_1 x^3 + p_2 x^2 + p_3 x + p_4$$

y usando multiplicación anidada

$$ P_3(x) = ((p_1 x + p_2) x + p_3) x + p_4$$

Considere cuántas operaciones se necesitan para cada...

$$ P_3(x) = p_1 x^3 + p_2 x^2 + p_3 x + p_4$$

$$P_3(x) = \overbrace{p_1 \cdot x \cdot x \cdot x}^3 + \overbrace{p_2 \cdot x \cdot x}^2 + \overbrace{p_3 \cdot x}^1 + p_4$$

Sumando todas las operaciones, en general podemos pensar en esto como una pirámide

podemos estimar de esta manera que el algoritmo escrito de esta manera tomará aproximadamente $\mathcal{O}(N^2/2)$ operaciones para completar.

Mirando nuetros otros medios de evaluación

$$ P_3(x) = ((p_1 x + p_2) x + p_3) x + p_4$$

Aquí encontramos que el método es $\mathcal{O}(N)$ (el 2 generalmente se ignora en estos casos). Lo importante es que la primera evaluación es $\mathcal{O}(N^2)$ y la segunda $\mathcal{O}(N)$!

[Volver a la Tabla de Contenido](#TOC)

### Algoritmo

Complete la función e implemente el método de *Horner*

```python
def eval_poly(p, x):
    """Evaluates polynomial given coefficients p at x

    Function to evaluate a polynomial in order N operations.  The polynomial is defined as

    P(x) = p[0] x**n + p[1] x**(n-1) + ... + p[n-1] x + p[n]

    The value x should be a float.
    """
    pass
```

```python
def eval_poly(p, x):
    """Evaluates polynomial given coefficients p at x

    Function to evaluate a polynomial in order N operations.  The polynomial is defined as

    P(x) = p[0] x**n + p[1] x**(n-1) + ... + p[n-1] x + p[n]

    The value x should be a float.
    """
    ### ADD CODE HERE
    pass
```

```python
# Scalar version
def eval_poly(p, x):
    """Evaluates polynomial given coefficients p at x

    Function to evaluate a polynomial in order N operations.  The polynomial is defined as

    P(x) = p[0] x**n + p[1] x**(n-1) + ... + p[n-1] x + p[n]

    The value x should be a float.
    """

    y = p[0]
    for coefficient in p[1:]:
        y = y * x + coefficient

    return y

# Vectorized version
def eval_poly(p, x):
    """Evaluates polynomial given coefficients p at x

    Function to evaluate a polynomial in order N operations.  The polynomial is defined as

    P(x) = p[0] x**n + p[1] x**(n-1) + ... + p[n-1] x + p[n]

    The value x can by a NumPy ndarray.
    """

    y = numpy.ones(x.shape) * p[0]
    for coefficient in p[1:]:
        y = y * x + coefficient

    return y

p = [1, -3, 10, 4, 5, 5]
x = numpy.linspace(-10, 10, 100)
plt.plot(x, eval_poly(p, x))
plt.show()
```

[Volver a la Tabla de Contenido](#TOC)


```python
#hide
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
import scipy.stats
import seaborn as sns
import random
import altair as alt
```

```python
#hide
import warnings
warnings.filterwarnings("ignore")
```

# "Central Limit Theorem & z-statistics"
> "Beginner's guide to central limit theorem and z-statistics"

- author: Jae Kim
- toc: false
- branch: master
- badges: true
- comments: true
- image: https://res.cloudinary.com/dbxctsqiw/image/upload/v1617581938/normal_dist_moajik.png
- categories: [statistics, central limit theorem, z-statistics]
- hide: false

// width of the content area
// can be set as "px" or "%"
$content-width:    600px;
$on-palm:          600px;
$on-laptop:        600px;
$on-medium:        600px;
$on-large:         600px;

# Summary

Central Limit Theorem

1) Many statistical tests and scores (including t-test and z-score) **assume that the population distribution follows normal distribution.**

2) However, many real life data do not follow normal distribution.

3) Central limit theorem (CLT) allows you to assume that the mean of any data is normally distributed **as long as your sample size is large enough.**

4) Skewness and kurtosis can be used to test the normality of a distribution. **Normal distribution has both skewness and kurtosis of 0.** If your distribution has skewness and kurtosis significantly different from 0, you may want to increase your sample size. For formal normality test using skewness and kurtosis, check out [Jarque–Bera test](https://en.wikipedia.org/wiki/Jarque%E2%80%93Bera_test).

5) The more samples you take, the closer the distribution of means follow normal distribution. But increasing the sample size is often very expensive in real life. Imagine having to increase the size of your clinical trial. So there is a trade off, you can either ensure the normality by increasing the sample size or risk working with a distribution that may differ significantly from normal distribution.

**You barely have a dataset containing the entire population! All these statistical procedures are to approximate the population data from whatever data we have access to!**

For formal proof of central limit theorem, check [this paper.](https://www.cs.toronto.edu/~yuvalf/CLT.pdf)

Normal Distribution (Gaussian Distribution)

Normal distribution is perhaps one of the most widely used distributions. It has many unique properties but ones that I find relevant to our lectures are listed below.

1) Normal distribution is defined by two parameters, mean ($\mu$) and standard variation ($\sigma$). **Once you know the two parameters, you can map the entire probability distribution function (pdf)**.

$$
x \backsim N(\mu, \sigma^2)
$$
Above expression simply means random variable x is distributed according to a normal distribution with mean $\mu$ and variance $\sigma^2$ (variance = $\text{standard deviation}^2$). Although some mathematical expressions can
look intimidating, most of them have really simple meaning.

$$
p(x) = \frac{1}{\sigma \sqrt{2\pi}} e^{-\frac{1}{2} \left({\frac{x-\mu}{\sigma}}\right)^2}
$$
Above is the pdf of a normal distribution. $\pi$ and $e$ are constants, meaning $p(x)$ only depends on $x, \mu$ and $\sigma$.

2) **68, 95, 99.7 rule.**

Approximately 68%, 95% and 99.7% of values in the distribution are within 1, 2 and 3 SDs of the mean, i.e., above or below. This allows easier probablistic interpretation of data (more in the next paragraph).

```python
#collapse
x_domain = np.arange(-4, 4, 0.01)
norm = scipy.stats.norm(loc = 0, scale = 1)
y = norm.pdf(x_domain)

plt.figure(figsize=(10, 6))
plt.plot(x_domain, y)
ylim = 0.7
plt.ylim(0, ylim)
plt.xlim(-4, 4)
plt.axvline(x = 1, ymax=norm.pdf(1)/ylim, lw = 1, color = 'blue')
plt.axvline(x = -1, ymax=norm.pdf(-1)/ylim, lw = 1, color = 'blue')
plt.axvline(x = 2, ymax=0.63, lw = 2, linestyle = ':')
plt.axvline(x = -2, ymax=0.63, lw = 2, linestyle = ':')

plt.axvline(x = 3, ymax=0.85, lw = 2, linestyle = ':')
plt.axvline(x = -3, ymax=0.85, lw = 2, linestyle = ':')

plt.axvline(x = 3, ymax=norm.pdf(3)/ylim, lw = 1, color = 'blue')
plt.axvline(x = -3, ymax=norm.pdf(-3)/ylim, lw = 1, color = 'blue')

plt.arrow(-0.9, 0.2, 1.8, 0, head_width=0.03, head_length=0.1, linewidth=2, length_includes_head=True, color = 'navy')
plt.arrow(0.9, 0.2, -1.8, 0, head_width=0.03, head_length=0.1, linewidth=2, length_includes_head=True, color = 'navy')

plt.arrow(-1.9, 0.45, 3.8, 0, head_width=0.03, head_length=0.1, linewidth=2, length_includes_head=True, color = 'navy')
plt.arrow(1.9, 0.45, -3.8, 0, head_width=0.03, head_length=0.1, linewidth=2, length_includes_head=True, color = 'navy')

plt.arrow(-2.8, 0.6, 5.8, 0, head_width=0.03, head_length=0.1, linewidth=2, length_includes_head=True, color = 'navy')
plt.arrow(2.8, 0.6, -5.8, 0, head_width=0.03, head_length=0.1, linewidth=2, length_includes_head=True, color = 'navy')

plt.text(-0.4, 0.15, '68.25%', fontsize = 16)
plt.text(-0.4, 0.47, '95.44%', fontsize = 16)
plt.text(-0.4, 0.64, '99.73%', fontsize = 16)

plt.xlabel('Standard Deviation $\sigma$', fontsize = 13)
plt.ylabel('Probability', fontsize = 13)
plt.title('Normal Distribution Area Under Curve');
```

Area under curve represents the cumulative probability within the region. You can integrate the probability distribution function to obtain area under curve. PDF is merely a mathmatical function which can be easily integrated analytically.

3) Many variables that we see in nature appear to have a probability density function that approximates a normal distribution. If we think about random biological or physical processes, they can often be viewed as being affected by a large number of random processes with individually small effects. That's why disciplines such as bioengineering often assume normality. Check [this paper](https://aidanlyon.com/normal_distributions.pdf) to see why so many natural phenomena follow normal distribution.

Z score

Z-score allows you to express how far your measurement is from the population mean in terms of number of standard deviations.

Dialogue 1)

       Sam: I am 169cm tall and the population mean and standard deviation are 187cm and 9cm respectively.

       Annoyed Joe: I have no idea how significantly short you are.

Dialogue 2)

        Sam: "My height is 2 standard deviations lower than the population mean."
              or
             "The z-score of my height is -2."

        Delighted Joe: "Only 2.28% of the population is shorter than you given that height is normally distributed.
                        You are pretty significantly short."

As demonstrated in the above dialogue, z-score allows an immediate understanding of the statistical significance of your data!

```python
#collapse
fig, ax= plt.subplots()
x_domain = np.arange(-3, 3, 0.01)
norm = scipy.stats.norm(loc = 0, scale = 1)
y = norm.pdf(x_domain)

ax.plot(x_domain, y)
ax.axvline(x= -2, ymax = norm.pdf(95)/0.1)

section = np.arange(-3, -2, 0.01)
ax.fill_between(section,norm.pdf(section), color = 'red')
ax.text(-3, 0.07,round(norm.cdf(-2),4), fontsize = 14)

ax.set_ylim(0, 0.5)
ax.set_xlabel('Standard deviation $\sigma$')
ax.set_ylabel('Probability density')
ax.set_title('Standard Normal Distribution');
```

**Caveats**

1) In order to use z-score, **you need a good approximation of population mean and standard derivation.** This may not be the case. If your you don't have a very good estimation of population standard deviation, you should use one sample t-test rather than z-test

2) Similarly, if you are compariing two groups, you should use tests such as two sample t-test. If you are comparing two groups of cancer patients (novel treatment group vs control group), we are not comparing with a group against the entire population. We are comapring between two groups.

2) Even if your new cancer treatment produces better clinical outcome compare to the entire population of cancer patients globally, it does not mean much. You want to have a strict control over the variables that might affect the outcome such as patient ethnicity, underlying medical conditions and the choice of treatment. As a result, many scientific research use t-test which allows the comparison between two groups of samples. Also, there are test you can use to compare more than 2 groups such as ANOVA test.

# Example Time

Now let's take a look at a real world dataset. Below is the distribution of disposable household income in the UK in 2020 [(source)](https://www.ons.gov.uk/peoplepopulationandcommunity/personalandhouseholdfinances/incomeandwealth/bulletins/householddisposableincomeandinequality/financialyear2020).

```python
#hide
#Read CSV file
path = 'dataset/uk_income_distribution_2020.xlsx'
df = pd.read_excel(path)
df.drop('Unnamed: 0', inplace = True, axis = 1)
df.columns = ['Income_band', 'Count(1000s)']

#The data provides household count. Procedure to make each row represents a sample.
dist_df = pd.DataFrame()
for i, j in df.iterrows():
    temp_df = pd.DataFrame((random.choices(range(int(j['Income_band']), int(j['Income_band']+1000)), k= int(j['Count(1000s)']))))
    dist_df = pd.concat([dist_df, temp_df])
dist_df.columns = ['income']
```

```python
#collapse
plt.figure(figsize=(15, 7))
ax = sns.histplot(dist_df, bins = 400, legend = False, kde = True)
plt.axvline(29900, color = 'red', lw = 3)
plt.axvline(36900, color = 'green', lw = 3)
#tick_spacing = 10000
#ax.xaxis.set_major_locator(ticker.MultipleLocator(tick_spacing))
plt.xticks(rotation = 45)
plt.xlim(0, 200000)
plt.ylim(0, 4000)
plt.title('Distribution of UK household disposable income, financial year ending 2020', fontsize = 15)
plt.xlabel('Equivalised disposable household income', fontsize=13)
plt.ylabel('Number of individuals (in 1000s)', fontsize = 12)

plt.text(12000, 3500, 'Median:\n £29,900', fontsize = 14, color = 'red')
plt.text(40000, 2500, 'Mean:\n £36,900', fontsize = 14, color = 'green');
```

From the first glance we can notice that the distribution
- does not follow normal distribution (unsymmetrical, highly skewed),
- is positively (right) skewed (mean < median).

Let's look at some basic statistics.

```python
print('Median: ', np.median(dist_df['income']))
dist_df.describe()
```

    Median:  30469.5

      income

      count
      65614.000000

      mean
      37496.055461

      std
      36260.932412

      min
      4.000000

      25%
      20706.250000

      50%
      30469.500000

      75%
      44731.750000

      max
      674891.000000

1. The data count represents about 65 million individuals (1 count for 1000 individuals). The entire population of the UK is about 67 million, so we have the data on almost the entire population.

2. Mean is larger than median, indicating positive skew.

3. Standard deviation is about £36,260.

Let's check the skewness and kurtosis to check how far the distribution is from normal distribution.

```python
print('Skewness: ',scipy.stats.skew(dist_df['income']))
print('Kurtosis: ',scipy.stats.kurtosis(dist_df['income']))
```

    Skewness:  7.790516879065766
    Kurtosis:  92.99649511424367

Skewness and kurtosis are far from 0. The distribution is clearly not normal.

### Sample size and CLT

Now let's see how we can use CLT to converge to normal distribution.

```python
#collapse
fig ,axs = plt.subplots(3, 2, figsize = (13, 22))
sample_sizes = [10, 30, 50, 100, 300, 3000]
trial = 1000

for sample_size, ax in zip(sample_sizes, axs.flatten()):
    sample_mean = []
    for i in (range(trial)):
        temp_list = random.choices(list(dist_df['income']), k = sample_size)
        sample_mean.append(np.mean(temp_list))
    sns.distplot(sample_mean, ax=ax)
    skew = scipy.stats.skew(sample_mean)
    kurtosis = scipy.stats.kurtosis(sample_mean)
    std = np.std(sample_mean)
    mean = np.mean(sample_mean)

    ax.set_xlim(20000, 54000)

    ax.set_label

    ax.set_title('Sample size: %d \n Skewness: %.2f, Kurtosis: %.2f' %(sample_size, skew, kurtosis))

    x0, xmax = ax.set_xlim()
    y0, ymax = ax.set_ylim()
    data_width = xmax - x0
    data_height = ymax - y0
    ax.text(x0 + data_width * 0.55, y0 + data_height * 0.8, 'std of sample means: %.2f \n mean: %.2f' %(std, mean), fontsize = 10)
```

6 samples sizes are used to draw random samples 1000 times and the mean of each sample is plotted in histograms.

1) As the sample size increases, the distribution more closely converges to normal distribution. You can see how skewness and kurtosis approach 0 with increasing sample size.

2) While mean stays relatively constant, standard deviation decreases with increasing sample size.
    Concretely, sample mean standard deviation can be estimated from sample size and population standard deviation:

$$
\text{standard error} = \frac{\sigma}{\sqrt{n}}
$$
where $\text{standard error}$ is approximate standard deviation of a sample population, $\sigma$ and $n$ are population standard deviation and sample size respectively.

Recall that our population std was about 36,500. Let's calculate the standard error for sample size of 3,000  and compare to our empirical standard deviation of sample population.

```python
standard_error = np.std(dist_df['income']) / np.sqrt(3000)
print(standard_error)
```

    662.0259763627873

Standard error is 662 while the empirical value is 656. This is a pretty good approximation. Let's use the standard error with sample size 3000 for our probablistic interpretation!

### Z Score and Probablistic Interpretation

Instead of using the original distribution which was highly skewed, we can now use a nice normal distribution for probablistic interpretation. Our normal distribution can be expressed defined by its mean and standard error.
$$
x \backsim N(37530, 656^2)
$$

Let's say you surveyed the disposable income fo 3000 people randomly. The mean of the sample population was £39000. Let's check its how significant it is using z-score.

$$
\begin{align}
Z & = \frac{x-\mu}{\sigma}\\
& = \frac{39000-37530}{656}\\
& \approx 2.24
\end{align}
$$

Your observation is 2.24 standard deviations away from mean! It is actually very unlikely to suvey 3000 people and get the average of 39000!

```python
#collapse
norm = scipy.stats.norm(loc = 0, scale = 1)
x_domain = np.arange(-4, 4, 0.01)
y = norm.pdf(x_domain)
cdf = norm.cdf(x_domain)
norm_df = pd.DataFrame()
norm_df['x'] = x_domain
norm_df['y'] = y
norm_df['cdf'] = cdf
```

```python
#collapse
slider = alt.binding_range(min=-4, max=4, step=0.1, name='Z score:')
selector = alt.selection_single(name="SelectorName", fields=['cutoff'],
                                bind=slider, init={'cutoff': -2.2})

chart_pdf = alt.Chart(norm_df).mark_bar().encode(
            alt.X('x:Q', title = 'Standard deviation'),
            alt.Y('y:Q', title = 'Probability density'),
            color = alt.condition(
                alt.datum.x <= selector.cutoff,
                alt.value('navy'),
                alt.value('lightgray')
                )
            ).add_selection(
                selector
            ).properties(
                title='Probability distribution function'
            )

chart_cdf = alt.Chart(norm_df).mark_bar().encode(
            alt.X('x:Q', title = 'Standard deviation'),
            alt.Y('cdf:Q', title = 'cumulative probability distribution'),
            color = alt.condition(
                alt.datum.x <= selector.cutoff,
                alt.value('navy'),
                alt.value('lightgray')
                )
            ).add_selection(
                selector
            ).properties(
                title = 'cumulative distribution function'
            )

chart_pdf|chart_cdf
```

You can use the above interactive chart to play with z score and see how the cumulative probability distribution changes.

```python
norm = scipy.stats.norm(loc = 0, scale = 1)
norm.cdf(-2.24)*2
```

    0.025090922871893125

From the culumative distribution function, we know that the probability of getting a sample mean further than 2.24 stds away from mean is only 2.5%.

So what does that imply?

1) It means you got a truly unlikely result only if you have truly randomly sampled from the entire population.

2) If you have only sampled from your neighborhood and thereby imposed control over your sample population, it can be a good reason to reject the null hypothesis and conclude that the difference between the income in your neightborhood is significantly different than the national average.

**Null hypothesis**: The average income in your neighborhood does not differ from the national average.

**Alternative hypothesis**: The average income in your neighborhood differ from the national average.

You can reject the null hypothesis with the p-value of 0.025. This means if the null hypothesis holds, there is only 2.5% chance of getting a result such as yours by pure chance.


# Features and Objectives

This doc is mostly text, explaining the general concept of features, listing the ones defined in rai, and explaining how they define objectives for optimization.

At the bottom there are also examples on the collision features.

## Features

We assume a single configuration $x$, or a whole set of configurations
$\{x_1,..,x_T\}$. Each $x_i \in\mathbb{R}$ are the DOFs of that
configuration.

A feature $\phi$ is a differentiable mapping
$$\phi: x \mapsto \mathbb{R}^D$$
of a single configuration into some $D$-dimensional space, or a mapping
$$\phi: (x_0,x_2,..,x_k) \mapsto \mathbb{R}^D$$
of a $(k+1)$-tuple of configurations to a $D$-dimensional space.

The rai code implements many features, most of them are accessible via
a feature symbol (FS). They are declared in
https://github.com/MarcToussaint/rai/blob/master/rai/Kin/featureSymbols.h

Here is a table of feature symbols, with the
respective dimensionality $D$, the default order $k$, and a
description

| FS | frames | $D$ | $k$ | description |
|:---:|:---:|:---:|:---:|:---:|
| position | {o1} | 3 || 3D position of o1 in world coordinates |
| positionDiff | {o1,o2} | 3 || difference of 3D positions of o1 and o2 in world coordinates |
| positionRel | {o1,o2} | 3 || 3D position of o1 in o2 coordinates |
| quaternion | {o1} | 4 || 4D quaternion of o1 in world coordinates\footnote{There is ways to handle the invariance w.r.t.\ quaternion sign properly.} |
| quaternionDiff | {o1,o2} | 4 || ... |
| quaternionRel | {o1,o2} | 4 || ... |
| pose | {o1} | 7 || 7D pose of o1 in world coordinates |
| poseDiff | {o1,o2} | 7 || ... |
| poseRel | {o1,o2} | 7 || ... |
| vectorX | {o1} | 3 || The x-axis of frame o1 rotated back to world coordinates |
| vectorXDiff | {o1,o2} | 3 || The difference of the above for two frames o1 and o2 |
| vectorXRel | {o1,o2} | 3 || The x-axis of frame o1 rotated as to be seend from the frame o2 |
| vectorY... | | | | same as above |
| scalarProductXX | {o1,o2} | 1 || The scalar product of the x-axis fo frame o1 with the x-axis of frame o2 |
| scalarProduct... | {o1,o2} | | | as above |
| gazeAt | {o1,o2} | 2 | | The 2D projection of the origin of frame o2 onto the xy-plane of frame o1 |
| angularVel | {o1} | 3 | 1 | The angular velocity of frame o1 across two configurations |
| accumulatedCollisions | {} | 1 | | The sum of collision penetrations; when negative/zero, nothing is colliding |
| jointLimits | {} | 1 | | The sum of joint limit penetrations; when negative/zero, all joint limits are ok |
| distance | {o1,o1} | 1 | | The NEGATIVE distance between convex meshes o1 and o2, positive for penetration |
| qItself | {} | $n$ | | The configuration joint vector |
| aboveBox | {o1,o2} | 4 | | when all negative, o1 is above (inside support of) the box o2 |
| insideBox | {o1,o2} | 6 | | when all negative, o1 is inside the box o2 |
| standingAbove | | | | ? |

A features is typically defined by
* The feature symbol (`FS_...` in cpp; `FS....` in python)
* The set of frames it refers to
* Optionally: A target, which changes the zero-point of the features (optimization typically try to drive features to zero, see below)
* Optionally: A scaling, that can also be a matrix to down-project a feature
* Optionally: The order $k$, which can make the feature a velocity or acceleration feature

Target and scale redefine a feature to become
$$
  \phi(x) \gets \texttt{scale} \cdot (\phi(x) - \texttt{target})
$$
The target needs to be a $D$-dim vector. The scale can be a matrix, which projects features; e.g., and 3D position to just $x$-position.

The order of a feature is usually $k=0$, meaning that it is defined over a single configuration only. $k=1$ means that it is defined over two configurations (1st oder Markov), and redefines the feature to become the difference or velocity
$$
  \phi(x_1,x_2) \gets \frac{1}{\tau}(\phi(x_2) - \phi(x_1))
$$
$k=2$ means that it is defined over three configurations (2nd order Markov), and redefines the feature to become the acceleration
$$
  \phi(x_1,x_2,x_3) \equiv \frac{1}{\tau^2}(\phi(x_1) + \phi(x_3) - 2 \phi(x_2))
$$

### Examples

```
(FS.position, {'hand'})
```
is the 3D position of the hand in world coordinates

```
(FS.positionRel, {'handL', 'handR'}, scale=[[0,0,1]], target=[0.1])
```
is the z-position position of the left hand measured in the frame of the right hand, with target 10centimeters.

```
(FS.position, {'handL'}, order=1)
```
is the 3D velocity of the left hand in world coordinates

```
(FS.scalarProductXX, {'handL', 'handR'}, target=[1])
```
says that the scalar product of the x-axes (e.g. directions of the index finger) of both hands should equal 1, which means they are aligned.

```
(FS.scalarProductXY, {'handL', 'handR'})
(FS.scalarProductXZ, {'handL', 'handR'})
```
says that the the x-axis of handL should be orthogonal (zero scalar product) to the y- and z-axis of handR. So this also describes aligning both x-axes. However, this formulation is much more robust, as it has good error gradients around the optimum.

## Objectives

Features are meant to define objectives in an optimization problem. An objective is
* a feature
* an indicator $\rho_k
\in\{\texttt{ineq, eq, sos}\}$ that states whether the features
implies an inequality, an equality, or a sum-of-square objective
* and an index tuple $\pi_k \subseteq \{1,..,n\}$ that states which
configurations this feature is defined over.

Then, given a set
$\{\phi_1,..,\phi_K\}$ of $K$ features, and a set $\{x_1,..,x_n\}$ of
$n$ configurations, this defines the mathematical program

\begin{align}
  \min_{x_1,..,x_n} \sum_{k : \rho_k=\texttt{sos}} \phi_k(x_{\pi_k})^T \phi_k(x_{\pi_k})
  ~\text{s.t.}~ \mathop\forall_{k : \rho_k=\texttt{ineq}} \phi_k(x_{\pi_k}) \le 0 ~,\quad
  \mathop\forall_{k : \rho_k=\texttt{eq}} \phi_k(x_{\pi_k}) = 0 ~,\quad
\end{align}

## Code example for collision features

* Get list of collisions and proximities for the whole configuration
* Get a accumulative, differentiable collision measure
* Get proximity/penetration specifically for a pair of shapes
* Other geometric collision features for a pair of shapes (witness points, normal, etc) -- all differentiable

```python
import sys
sys.path += ['../build', '../../../build', '../../lib']
import numpy as np
import libry as ry

C = ry.Config()
C.addFile('../../../rai-robotModels/pr2/pr2.g');
C.addFile('../../../rai-robotModels/objects/kitchen.g');

C.view()
```

Let's evaluate the accumulative collision scalar and its Jacobian

```python
coll = C.feature(ry.FS.accumulatedCollisions, [])

C.computeCollisions() #collisions/proxies are not automatically computed on set...State
coll.eval(C)
```

Let's move into collision and redo this

```python
C.selectJointsByTag(["base"])
C.setJointState([1.5,1,0])

C.computeCollisions()
coll.eval(C)
```

We can get more verbose information like this:

```python
C.getCollisions()
```

```python
C.getCollisions(0) #only report proxies with distance<0 (penetrations)
```

The computeCollisions() method calls a collision detection engine (SWIFT++) for the whole configuration, checking all shapes that are collision-activated. The activation/deactivation of collision computations is a nuissance! the 'contact' flag in g-files specifies which shapes are activated by default, and if the value is negative, that collisions with parent shapes are not included. (In the KOMO class, you can use activateCollisionPairs and deactivateCollisionPairs to modify these defaults in optimization problems... TODO: also in Config)

When you're interested in the distance or penetration of one specific pair of objects, you don't need to call computeCollisions() and instead query a feature that calls the GJK (and others) algorithm directly only for this pair:

```python
dist = C.feature(ry.FS.distance, ['coll_wrist_r', '_10'])
dist.eval(C)
```

Note that this returns the NEGATIVE distance (because one typically wants to put an inequality (<=0) on this). The C++ code implements many more features of the collision geometry, including the normal, witness points, etc. Can be added to python easily on request.

```python
C.view_close()
```

```python

```


+ This notebook is part of the addition lecture *An overview of key ideas* in the OCW MIT course 18.06 by Prof Gilbert Strang [1]
+ Created by me, Dr Juan H Klopper
    + Head of Acute Care Surgery
    + Groote Schuur Hospital
    + University Cape Town
    + Email me with your thoughts, comments, suggestions and corrections

Linear Algebra OCW MIT18.06 IPython notebook [2] study notes by Dr Juan H Klopper is licensed under a Creative Commons Attribution-NonCommercial 4.0 International License.

+ [1] OCW MIT 18.06
+ [2] Fernando Pérez, Brian E. Granger, IPython: A System for Interactive Scientific Computing, Computing in Science and Engineering, vol. 9, no. 3, pp. 21-29, May/June 2007, doi:10.1109/MCSE.2007.53. URL: http://ipython.org

```python
from IPython.core.display import HTML, Image
css_file = 'style.css'
HTML(open(css_file, 'r').read())
```

<link href='http://fonts.googleapis.com/css?family=Alegreya+Sans:100,300,400,500,700,800,900,100italic,300italic,400italic,500italic,700italic,800italic,900italic' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Arvo:400,700,400italic' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=PT+Mono' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Shadows+Into+Light' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Philosopher:400,700,400italic,700italic' rel='stylesheet' type='text/css'>

```python
from sympy import init_printing, Matrix, symbols, sqrt, Rational
from numpy import matrix, transpose, sqrt
from numpy.linalg import pinv, inv, det, svd, norm
from scipy.linalg import pinv2
from warnings import filterwarnings
```

```python
init_printing(use_latex = 'mathjax')
filterwarnings('ignore')
```

# An overview of key ideas

## Moving from vectors to matrices

+ Consider a position vector in three-dimensional space
+ It can be written as a column-vector
$$ u=\begin{bmatrix}1\\-1\\0\end{bmatrix} \\ v=\begin{bmatrix}0\\1\\-1\end{bmatrix} $$
+ We can add constant scalar multiples of these vectors
$$ {x}_{1}{u}+{x}_{2}{v}={b} $$
+ This is simple vector addition
+ Its easy to visualize that if we combine all possible combinations, that we start filling a plane through the origin
+ Adding a third vector that is not in this plane will extend all possible linear combinations to fill all of three-dimensional space
$$ w=\begin{bmatrix}0\\0\\1\end{bmatrix} $$
+ We now have the following
$$ {x}_{1}{u}+{x}_{2}{v}+{x}_{3}{w}={b} $$

+ Notice how this last equation can be written in matrix form A**x**=**b**
$$ \begin{bmatrix} 1 & 0 & 0 \\ -1 & 1 & 0 \\ 0 & -1 & 1 \end{bmatrix}\begin{bmatrix} { x }_{ 1 } \\ { x }_{ 2 } \\ { x }_{ 3 } \end{bmatrix}=\begin{bmatrix} { x }_{ 1 } \\ { x }_{ 2 }-{ x }_{ 1 } \\ { x }_{ 3 }-{ x }_{ 2 } \end{bmatrix} $$
+ This is the column-view of matrix-vector multiplication as opposed to the row view
    + Matrices are seen a column, representing vectors
    + Each element of the column vector **x** is a scalar multiple of the corresponding column in the matrix A
    $$ { x }_{ 1 }\begin{bmatrix} 1 \\ -1 \\ 0 \end{bmatrix}+{ x }_{ 2 }\begin{bmatrix} 0 \\ 1 \\ -1 \end{bmatrix}+{ x }_{ 3 }\begin{bmatrix} 0 \\ 0 \\ 1 \end{bmatrix}=\begin{matrix} { x }_{ 1 } \\ -{ x }_{ 1 }+{ x }_{ 2 } \\ -{ x }_{ 2 }+{ x }_{ 3 } \end{matrix} = {x}_{1}{u}+{x}_{2}{v}+{x}_{3}{w}$$

+ Now consider the solution vector **b**
$$ \begin{bmatrix} 1 & 0 & 0 \\ -1 & 1 & 0 \\ 0 & -1 & 1 \end{bmatrix}\begin{bmatrix} { x }_{ 1 } \\ { x }_{ 2 } \\ { x }_{ 3 } \end{bmatrix}=\begin{bmatrix} { x }_{ 1 } \\ { x }_{ 2 }-{ x }_{ 1 } \\ { x }_{ 3 }-{ x }_{ 2 } \end{bmatrix} = \begin{bmatrix}{b}_{1}\\{b}_{2}\\{b}_{3}\end{bmatrix} $$
+ By substitution we we now have the following
$$ \begin{bmatrix} { x }_{ 1 } \\ { x }_{ 2 } \\ { x }_{ 3 } \end{bmatrix}=\begin{bmatrix} { b }_{ 1 } \\ { b }_{ 1 }+{ b }_{ 2 } \\ { b }_{ 1 }+{ b }_{ 2 }+{ b }_{ 2 } \end{bmatrix} $$
+ This, though, looks like a matrix times **b**
$$ \begin{bmatrix}1&0&0\\1&1&0\\1&1&1\end{bmatrix}\begin{bmatrix}{b}_{1}\\{b}_{2}\\{b}_{3}\end{bmatrix} $$
+ This matrix is the inverse of A such that **x**=A-1**b**

+ The above matrix A is called a difference matrix as it took simple differences between the elements of vector **x**
+ It was lower triangular
+ Its inverse became a sum matrix
+ So it was a good matrix, able to transform between **x** and **b** (back-and-forth) and therefor invertible and for every **x** has a specific inverse
+ It transforms **x** into **b** (maps)

+ Let's look at the code for this matrix which replaces **w** above

```python
x1, x2, x3, b1, b2, b3 = symbols('x1, x2, x3, b1, b2, b3') # Creating algebraic symbols
# This reserves these symbols so as not to see them as computer variable names
```

```python
C = Matrix([[1, 0, -1], [-1, 1, 0], [0, -1, 1]]) # Creating a matrix and putting
# it into a computer variable called C
C # Displaying it to the screen
```

$$\left[\begin{matrix}1 & 0 & -1\\-1 & 1 & 0\\0 & -1 & 1\end{matrix}\right]$$

```python
x_vect = Matrix([[x1], [x2], [x3]]) # Giving this columns vector a computer
# variable name
x_vect
```

$$\left[\begin{matrix}x_{1}\\x_{2}\\x_{3}\end{matrix}\right]$$

```python
C * x_vect
```

$$\left[\begin{matrix}x_{1} - x_{3}\\- x_{1} + x_{2}\\- x_{2} + x_{3}\end{matrix}\right]$$

+ We now have three equations
$$ { x }_{ 1 }-{ x }_{ 3 }={ b }_{ 1 }\\ { x }_{ 2 }-{ x }_{ 1 }={ b }_{ 2 }\\ { x }_{ 3 }-{ x }_{ 2 }={ b }_{ 3 } $$
+ Adding the left and right sides we get the following
$$ 0={ b }_{ 1 }+{ b }_{ 2 }+{ b }_{ 3 } $$
+ We are now constrained for values of *b*i

+ The problem is clear to see geometrically as the new **w** is in the same plane as **u** and **v**
+ In essence **w** did not add anything
+ All combinations of **u**, **v**, and **w** will still be in the plane
+ The first matrix A above had three independent columns and their linear combinations could fill all of three-dimensional space
+ That made the first matrix A invertible as opposed to the second one (C), which is not invertible (i.e. it cannot take any vector in three-dimensional space back to **x**)

+ Let's look at the original column vectors in C
+ Remember the following dot product
$$ {a}\cdot{b}=||a||||b||\cos{\theta} \\ \cos\left(\pi\right)=-1 $$
+ In linear algebra getting the dot product of two vectors is written as follows
$$ a\cdot{b}={b}^{T}{a} $$
+ Which is the transpose of the second times the first

```python
u = Matrix([[1], [-1], [0]])
v = Matrix([[0], [1], [-1]])
w = Matrix([[-1], [0], [1]])
u, v, w
```

$$\begin{pmatrix}\left[\begin{matrix}1\\-1\\0\end{matrix}\right], & \left[\begin{matrix}0\\1\\-1\end{matrix}\right], & \left[\begin{matrix}-1\\0\\1\end{matrix}\right]\end{pmatrix}$$

```python
v.transpose() * u
```

$$\left[\begin{matrix}-1\end{matrix}\right]$$

```python
w.transpose() * u
```

$$\left[\begin{matrix}-1\end{matrix}\right]$$

```python
w.transpose() * v
```

$$\left[\begin{matrix}-1\end{matrix}\right]$$

```python
u.transpose() * v
```

$$\left[\begin{matrix}-1\end{matrix}\right]$$

```python
u.transpose() * w
```

$$\left[\begin{matrix}-1\end{matrix}\right]$$

```python
v.transpose() * w
```

$$\left[\begin{matrix}-1\end{matrix}\right]$$

+ The angle between all of them is π radians and therefor they must all lie in a plane

## Example problems

### Example problem 1

+ Suppose A is a matrix with the following solution
$$ {A}{x}=\begin{bmatrix}1\\4\\1\\1\end{bmatrix} \\ {x}=\begin{bmatrix}0\\1\\1\end{bmatrix}+{c}\begin{bmatrix}0\\2\\1\end{bmatrix} $$
+ What can you say about the columns of A?

#### Solution

```python
c = symbols('c')
x_vect = Matrix([[0], [1 + 2 * c], [1 + c]])
b = Matrix([[1], [4], [1], [1]])
```

+ **x** is of size *m* × *n* is 3 × 1
+ **b** is of size 4 × 1
+ Therefor A must be of size 4 × 3 and each column vector in A is in ℝ4

+ Let's call these columns of A *C*1, *C*2, and *C*3
$$ \begin{bmatrix} \vdots  & \vdots  & \vdots  \\ { C }_{ 1 } & { C }_{ 2 } & { C }_{ 3 } \\ \vdots  & \vdots  & \vdots  \\ \vdots  & \vdots  & \vdots  \end{bmatrix} $$

+ With the particular way in which **x** was written we can say that we have a particular solution and a special solution
$$ {A}\left({x}_{p}+{c}\cdot{x}_{s}\right)=b $$
+ For *c* = 0 we have:
$$ {A}{x}_{p}=b $$
+ For *c* = 1 we have:
$$ A{ x }_{ p }+A{ x }_{ s }=b\\ \because \quad A{ x }_{ p }=b\\ b+A{ x }_{ s }=b\\ \therefore \quad A{ x }_{ s }=0 $$

+ We also have that the following
$$ { x }_{ p }=\begin{bmatrix} 0 \\ 1 \\ 1 \end{bmatrix},\quad { x }_{ s }=\begin{bmatrix} 0 \\ 2 \\ 1 \end{bmatrix} $$

+ For *x*p we have the following
$$ \begin{bmatrix} \vdots  & \vdots  & \vdots  \\ { C }_{ 1 } & { C }_{ 2 } & { C }_{ 3 } \\ \vdots  & \vdots  & \vdots  \\ \vdots  & \vdots  & \vdots  \end{bmatrix}\begin{bmatrix} 0 \\ 1 \\ 1 \end{bmatrix}=b\quad \Rightarrow \quad { C }_{ 2 }+{ C }_{ 3 }=b $$
+ For *x*s we have the following
$$ \begin{bmatrix} \vdots  & \vdots  & \vdots  \\ { C }_{ 1 } & { C }_{ 2 } & { C }_{ 3 } \\ \vdots  & \vdots  & \vdots  \\ \vdots  & \vdots  & \vdots  \end{bmatrix}\begin{bmatrix} 0 \\ 2 \\ 1 \end{bmatrix}=\underline { 0 } \quad \Rightarrow \quad 2{ C }_{ 2 }+{ C }_{ 3 }=0 $$

+ Solving for *C*2 and *C*3 we have the following
$$ {C}_{3}=-2{C}_{2} \\ {C}_{2}-2{C}_{2}=b \\ {C}_{2}=-b \\ {C}_{3}=2b$$

+ As for the first column of A, we need to know more about ranks and subspaces
+ We see, though, that columns 2 and three are already constant multiples of each other
+ So, as long as column 1 is not a constant multiple of b, we are safe
$$ A=\begin{bmatrix} \vdots  & 1 & 2 \\ { C }_{ 1 } & 4 & 8 \\ \vdots  & 1 & 2 \\ \vdots  & 1 & 2 \end{bmatrix} $$

```python

```


# The Capacitated Vehicle Routing Problem

A short example of using Gurobi to model and solve the CRVP

> Vehicle routing problems are inherently intractable: the length of time it takes to solve them grows exponentially with the size of the problem. For sufficiently large problems, it could take OR-tools (or any other routing software) years to find the optimal solution..

```python
import numpy as np
from numpy.random import RandomState
import matplotlib.pyplot as plt
import seaborn as sns
sns.set()

```

```python
#or tools imports
from ortools.constraint_solver import routing_enums_pb2
from ortools.constraint_solver import pywrapcp
```

## Parameters

```python
N_PATIENTS = 15
SEED = 101
```

## Patient Geographic Location

```python
#random number generator for synthetic data.
rand_gen = RandomState(seed=SEED)

#n_patients = 1 so we include the depot
x_coords = rand_gen.random(size=N_PATIENTS+1) * 200
y_coords = rand_gen.random(size=N_PATIENTS+1) * 100

coords = [[x, y] for x, y in zip(x_coords, y_coords)]
```

```python
def plot_patient_coordinates(x_coords, y_coords):
    '''
    Plot a simple scatter of coordinates.
    Depot is red, customers are blue

    Paramters:
    x_coords: array-like
        x coordinates for each patient. element 0 = depot.

    y_coords: array-like
        y coordinates for each patient. element 0 = depot
    '''
    plt.plot(x_coords[0], y_coords[0], c='r', marker='^')
    #plt.scatter(x_coords[1:], y_coords[1:], c='b')

    for i in range(1, len(x_coords[1:])):
        x = x_coords[i]
        y = y_coords[i]
        plt.plot(x, y, 'b*')
        plt.text(x * (1 + 0.01), y * (1 + 0.01) , i, fontsize=12)

    plt.xlim((0, 220))
    plt.ylim((0, 120))
    plt.show()
```

```python
#visualise
plot_patient_coordinates(x_coords, y_coords)
```

## Approaches to 'solving' the CVRP

* Formulation as a Mixed Integer Programme (MIP) and use of a optimisation solver
* **Heuristic solution**.  E.g. using Iterated Local Search.

## Solution via meta-heuristic

```python
#Iterated Local Search - multiple runs of Ordinary Decent 2Opt
objective = SimpleTSPObjective(matrix)
local_search = OrdinaryDecent2OptNew(objective, tour)
runner = IteratedLocalSearch(objective, local_search, maximisation=False)
n = 20
print("\nRunning Iterated Local Search using Ordinary Decent 2-Opt ({} runs)..."\
      .format(n))
runner.run(n)
```

```python
def create_data_model(coords):
    data = {}
    data['distance_matrix'] = gen_matrix(np.array(coords))
    data['num_vehicles'] = round(N_PATIENTS / 4)
    data['depot'] = 0
    data['vehicle_capacities'] = [4 for i in range(data['num_vehicles'])]
    data['demands'] = [1 for i in range(N_PATIENTS)]
    return data
```

```python
"""Vehicles Routing Problem (VRP)."""

from __future__ import print_function
from ortools.constraint_solver import routing_enums_pb2
from ortools.constraint_solver import pywrapcp

def print_solution(data, manager, routing, solution):
    """Prints solution on console."""
    max_route_distance = 0
    total_distance = 0
    routes = []
    for vehicle_id in range(data['num_vehicles']):
        index = routing.Start(vehicle_id)
        plan_output = 'Route for vehicle {}:\n'.format(vehicle_id)
        route_distance = 0
        route = []
        while not routing.IsEnd(index):
            plan_output += ' {} -> '.format(manager.IndexToNode(index))
            route.append(manager.IndexToNode(index))
            previous_index = index
            index = solution.Value(routing.NextVar(index))
            route_distance += routing.GetArcCostForVehicle(
                previous_index, index, vehicle_id)
            total_distance += route_distance
        plan_output += '{}\n'.format(manager.IndexToNode(index))
        plan_output += 'Distance of the route: {}m\n'.format(route_distance)
        print(plan_output)
        max_route_distance = max(route_distance, max_route_distance)
        routes.append(route)
    print('Maximum of the route distances: {}m'.format(max_route_distance))
    print('total distance: {}m'.format(total_distance))
    return routes

def main():
    """Solve the CVRP problem."""
    # Instantiate the data problem.
    data = create_data_model(coords)

    # Create the routing index manager.
    manager = pywrapcp.RoutingIndexManager(len(data['distance_matrix']),
                                           data['num_vehicles'], data['depot'])

    # Create Routing Model.
    routing = pywrapcp.RoutingModel(manager)

    # Create and register a transit callback.
    def distance_callback(from_index, to_index):
        """Returns the distance between the two nodes."""
        # Convert from routing variable Index to distance matrix NodeIndex.
        from_node = manager.IndexToNode(from_index)
        to_node = manager.IndexToNode(to_index)
        return data['distance_matrix'][from_node][to_node]

    transit_callback_index = routing.RegisterTransitCallback(distance_callback)

    # Define cost of each arc.
    routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index)

    # Add Capacity constraint.
    def demand_callback(from_index):
        """Returns the demand of the node."""
        # Convert from routing variable Index to demands NodeIndex.
        from_node = manager.IndexToNode(from_index)
        return data['demands'][from_node]

    demand_callback_index = routing.RegisterUnaryTransitCallback(
        demand_callback)
    routing.AddDimensionWithVehicleCapacity(
        demand_callback_index,
        0,  # null capacity slack
        data['vehicle_capacities'],  # vehicle maximum capacities
        True,  # start cumul to zero
        'Capacity')

    # Setting first solution heuristic.
    search_parameters = pywrapcp.DefaultRoutingSearchParameters()
    search_parameters.first_solution_strategy = (
        routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC)

    #time limit
    search_parameters.time_limit.seconds = 30

    # Solve the problem.
    solution = routing.SolveWithParameters(search_parameters)

    # Print solution on console.
    if solution:
        routes = print_solution(data, manager, routing, solution)

    print(routes)

```

```python
main()
```

    Route for vehicle 0:
     0 ->  2 ->  1 -> 0
    Distance of the route: 0m

    Route for vehicle 1:
     0 ->  6 ->  5 ->  4 ->  3 -> 0
    Distance of the route: 0m

    Route for vehicle 2:
     0 ->  10 ->  9 ->  8 ->  7 -> 0
    Distance of the route: 0m

    Route for vehicle 3:
     0 ->  15 ->  14 ->  13 ->  12 ->  11 -> 0
    Distance of the route: 0m

    Maximum of the route distances: 0m
    total distance: 0m
    [[0, 2, 1], [0, 6, 5, 4, 3], [0, 10, 9, 8, 7], [0, 15, 14, 13, 12, 11]]

```python
def plot_solution(x, instance):
    active_arcs = [a for a in instance['A'] if x[a].x == 1.00]
    xc = instance['x_coords']
    yc = instance['y_coords']

    for i, j in active_arcs:
        plt.plot([xc[i], xc[j]], [yc[i], yc[j]], c='green')
        plt.plot(xc[0], yc[0], c='r', marker='s')
        plt.scatter(xc[1:], yc[1:], c='b')
```

## Mixed Integer Programme (MIP) formulation

* The advantage of a mathematical forumulation of the problem is that we can use a modern solver like **Gurobi**.
* Gurobi is extremely powerful.  The downside is that a commerical tool and the source code is closed (but free to use in research).

$n$ is the number of patients

$N$ is the set of patients $\{1, 2, ..., n\}$

$V$ is the set of verticies (nodes) to visit. $V=\{0\}\cup N$

$A$ is the set of arcs (links) $A = \left\{ \left(i,j\right)  \in V^2 : i\neq j\right\}$

$c_{ij}$ is cost of travel over arc $(i,j)\in A$

$Q$ is the vehicle capacity (for ambulances this is an integer)

$q_i$ is the load represented by a patient (in our problem this always 1)

\begin{equation}
\begin{aligned}
       \text{min} &  \sum_{i=1}^{n} \sum_{j=1}^{n} c_{ij} x_{ij} \\
       \text{s.t.} & \sum_{i=1}^{n} x_{ij} = 1, & & j \in N, j \neq i \\
       &   \sum_{j=1}^{n} x_{ij} = 1, & & i \in N, i \neq j \\
       &  \text{if} \ x_{ij}=1 \ \Rightarrow \ u_i + q_j = u_j && i,j \in A : j\neq 0, i\neq 0 \\
       &  q_i \leq u_i \leq Q && i \in N \\
       &  x_{ij} \in \{0,1\} && i,j \in A
\end{aligned}
\end{equation}

```python
def problem_instance(n_patients, vehicle_capacity, random_state=None):
    '''
    Create an instance of the CVRP in the formulation
    of a MIP.

    Parameters:
    ------
    n_patient: int
    vehicle_capacity: int
    random_state: int, optional (default=None)

    Returns:
    -----
        dict, keys = N, V, A, c, Q, q
    '''
    rand_gen = RandomState(seed=random_state)

    #n_patients = 1 so we include the depot
    x_coords = rand_gen.random(size=n_patients+1) * 200
    y_coords = rand_gen.random(size=n_patients+1) * 100

    N = np.arange(1, n_patients+1)
    V = np.arange(0, n_patients+1)
    A = [(i, j) for i in V for j in V if i != j]
    c = {(i, j): np.hypot(x_coords[i]-x_coords[j], y_coords[i]-y_coords[j])
     for i, j in A}
    Q = vehicle_capacity
    q = {i: 1 for i in N}
    return {'x_coords':x_coords, 'y_coords':y_coords,
            'N':N, 'V':V, 'A':A, 'c':c, 'Q':Q, 'q':q}
```

```python
def optimisation_model(instance):
    '''
    Create and return an istance of a gurobi optimisation
    model for solving the CVRP.

    Parameters:
    -------
    instance: dict
        problem instance for the CVRP
    '''
    mdl = Model('dialysis_transport')

    N = instance['N']
    V = instance['V']
    A = instance['A']
    c = instance['c']
    Q = instance['Q']
    q = instance['q']

    x = mdl.addVars(A, vtype=GRB.BINARY)
    u = mdl.addVars(N, vtype=GRB.CONTINUOUS)
    mdl.modelSense = GRB.MINIMIZE
    mdl.setObjective(quicksum(x[a]*instance['c'][a] for a in A))
    mdl.addConstrs(quicksum(x[i, j] for j in V if j != i) == 1 for i in N)
    mdl.addConstrs(quicksum(x[i, j] for i in V if i != j) == 1 for j in N)
    mdl.addConstrs((x[i, j] == 1) >> (u[i]+q[j] == u[j])
                   for i, j in A if i != 0 and j != 0)
    mdl.addConstrs(u[i] >= q[i] for i in N)
    mdl.addConstrs(u[i] <= Q for i in N)
    return mdl, x, u
```

```python
def plot_solution(x, instance):
    active_arcs = [a for a in instance['A'] if x[a].x == 1.00]
    xc = instance['x_coords']
    yc = instance['y_coords']

    for i, j in active_arcs:
        plt.plot([xc[i], xc[j]], [yc[i], yc[j]], c='green')
        plt.plot(xc[0], yc[0], c='r', marker='s')
        plt.scatter(xc[1:], yc[1:], c='b')
```

```python
#model parameters
N_PATIENTS = 15
VEHICLE_CAPACITY = 4
TIME_LIMIT = 10
SEED = 101

#create a problem instance
instance = problem_instance(N_PATIENTS, VEHICLE_CAPACITY, random_state=SEED)

#create an instance of a gurobi optimisation model
model, x, u = optimisation_model(instance)

#limit run time?
if TIME_LIMIT > -1:
    model.Params.TimeLimit = TIME_LIMIT

#execute the optimisation procedure
model.optimize()

#show the routes
plot_solution(x, instance)
```

```python

```