# Tema 5: Interpolación de funciones

```python
!pip install -r https://raw.githubusercontent.com/alexmascension/ANMI/main/requirements.txt
```

```python
from sympy import *
from sympy.matrices import Matrix as mat
from sympy.matrices import randMatrix
from sympy import symbols
import sympy

import numpy as np
from tqdm import tqdm

from scipy.linalg import orth

from matplotlib import pyplot as plt
import matplotlib as mpl
mpl.rcParams['figure.dpi'] = 150
```

```python
from anmi.genericas import norma_p_func, norma_inf_func, matriz_inversa
from anmi.T5 import polinomio_lagrange, polinomio_newton, error_lagrange, error_maximo_estocastico, roots_chebyshev
from anmi.T5 import aitken_neville, interpolacion_hermite, polinomio_generico, esplines
```

```python
x, y, z, a, lambda_ = symbols('x'), symbols('y'), symbols('z'), symbols('a'), symbols('lambda')
```

Dado un conjunto de puntos (nodos) $x_0, x_1, \cdots, x_m$ podemos crear un polinomio $p$ que aproxime la función $f$ y que cumpla que $p(x_i) = f(x_i)$ para todo nodo. Existen varias situaciones de interpolación:
* El número de nodos menos 1, $m-1$ es MAYOR que el máximo grado de polinomios, $n$.
* El número de nodos menos 1, $m-1$ es IGUAL que el máximo grado de polinomios, $n$.
* El número de nodos menos 1, $m-1$ es MENOR que el máximo grado de polinomios, $n$.

En este capítulo nos centraremos cuando $m - 1 = n$.

## Interpolación de Lagrange

En esta interpolación empleamos un conjunto de funciones $L = \{l_0, l_1, \cdots, l_n\}$ correspondiente a los $n+1$ nodos $\{x_i:i = 0, 1, \cdots, n\}$. Cada $l_i$ se define como

$$l_i(x) = \prod_{k = 0 \neq i}\frac{x - x_k}{x_i - x_k}$$

Y así el polinomio resultante es

$$p(x) = \sum_{i=0}^n l_i(x)f(x_i)$$

Donde $f(x_i)$ es el valor de la función $f$ en $x_i$.

```python
help(polinomio_lagrange)
```

```python
f = E**(sin(4*x))
I = [0, 1]
x_vals = np.linspace(I[0], I[1], 4)
y_vals = [f.evalf(subs={x: S(i)}) for i in x_vals]
```

```python
p, lista_L = polinomio_lagrange(x_vals, y_vals)
```

```python
expand(p)
```

```python
x_range = np.linspace(I[0], I[1], 100)
y_real = [f.subs(x, i) for i in x_range]
y_pol = [expand(p).subs(x, i) for i in x_range]

plt.plot(x_range, y_real, label=f'{f}')
plt.scatter(x_vals, y_vals, c="#800000")
plt.plot(x_range, y_pol, label='interpolacion', c="#bcbcbc")

plt.legend()
```

Ahora ploteamos cada una de las funciones

```python
x_range = np.linspace(I[0], I[1], 100)
y_pol = [expand(p).subs(x, i) for i in x_range]
y_real = [f.subs(x, i) for i in x_range]

for p_i, pl in enumerate(lista_L):
    y_l = [expand(pl).subs(x, i) * y_vals[p_i] for i in x_range]
    plt.plot(x_range, y_l, label=f'$l_{p_i}$')
    plt.scatter(x_vals[p_i], y_vals[p_i])

plt.plot(x_range, y_real, label=f'{f}')
plt.plot(x_range, y_pol, label='interpolacion', c="#bcbcbc")

plt.legend(bbox_to_anchor=(1.05, 0.5))
```

## Método de Newton
El método de Lagrange, aunque da un polinomio único, tiene una serie de problemas. En primer lugar, aunque computacionalmente no parezca complejo, el cálculo de coeficientes es tedioso y poco práctico para ciertas funciones. Además, si queremos añadir un nuevo nodo, tenemos que recalcular todas las funciones de Lagrange para el polinomio nuevo, lo cual lo vuelve bastante impráctico.

Con el método de Newton solventamos las problemáticas estableciendo un método iterativo que incorpora un nodo por vez.

En el método se aplican los siguientes cálculos:

$$
\begin{matrix}
x_0 & \color{blue}{f(x_0)} & & \\
  & & \color{blue}{\frac{f(x_1) - f(x_0)}{x_1 - x_0}} & \\
x_1   & f(x_1) & & \color{blue}{\frac{\frac{f(x_2) - f(x_1)}{x_2 - x_1} - \frac{f(x_1) - f(x_0)}{x_1 - x_0}}{x_2 - x_0}}\\
    & & \frac{f(x_2) - f(x_1)}{x_2 - x_1}& \\
x_2     &f(x_2) & & \\
\vdots     & \vdots & & \\
x_{n-1}     &f(x_{n-1}) & & \\
& & \frac{f(x_n) - f(x_{n-1})}{x_n - x_{n-1}} & \\
x_n     &f(x_n) & & \\
\end{matrix}
$$

O de manera compacta:
$$
\begin{matrix}
x_0 & \color{blue}{f(x_0)} & & \\
  & & \color{blue}{f[x_0, x_1]} & \\
x_1   & f(x_1) & & \color{blue}{f[x_0, x_1, x_2]}\\
    & & \color{blue}{f[x_1, x_2]}& \\
x_2     &f(x_2) & & \\
\vdots     & \vdots & & \\
x_{n-1}     &f(x_{n-1}) & & \\
& & \color{blue}{f[x_{n-1}, x_n]} & \\
x_n     &f(x_n) & & \\
\end{matrix}
$$
Con
$$f[x_a, x_{a+1}, \cdots, x_j] = \frac{f[x_{a+1}, x_{a + 2}, \cdots, x_j] - f[x_{a}, x_{a+1}, \cdots, x_{j-1}]}{x_j - x_a}$$
Si denominamos $a_0, a_1, \cdots, a_n$ a los monomios en azul ($a_0 = f(x_0)$, $a_1 = \frac{f(x_1) - f(x_0)}{x_1 - x_0}$, etc.) entonces el polinomio de Newton se construye como:
$$p_n(x) = a_0 + a_1(x - x_0) + a_2(x - x_0)(x - x_1) + \cdots + a_n(x - x_0)(x - x_1)\cdots(x - x_{n-1})$$

```python
help(polinomio_newton)
```

```python
p, mat = polinomio_newton([S(i) for i in [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7]], [S(i) for i in [0.7, 0.8, 1, 1.15, 1.25, 1.3]], x)
mat
```

```python
p
```

```python
f = E**(sin(4*x))
I = [0, 1]
x_vals = np.linspace(I[0], I[1], 4)
y_vals = [f.evalf(subs={x: S(i)}) for i in x_vals]
```

```python
p, mat = polinomio_newton(x_vals, y_vals, x, evalf=5)
p
```

```python
mat.evalf(4)
```

```python
x_range = np.linspace(I[0], I[1], 100)
y_real = [f.subs(x, i) for i in x_range]
y_pol = [expand(p).subs(x, i) for i in x_range]

plt.plot(x_range, y_real, label=f'{f}')
plt.scatter(x_vals, y_vals, c="#800000")
plt.plot(x_range, y_pol, label='interpolacion', c="#bcbcbc")

plt.legend()
```

## Error en la interpolación de Lagrange
Dado un polinomio de orden $n$, se tiene que el error máximo viene estimado por la expresión:
$$E(x) = f(x) - p_n(x) = \frac{(x-x_0)(x-x_1)\cdots(x-x_n)}{(n+1)!}\max_{t\in[a,b]}f^{(n+1)}(t)$$

```python
help(error_lagrange)
help(error_maximo_estocastico)
```

#### EJERCICIO 48

```python
f = sqrt(x)
I = [0, 2]
grado = 4

x_vals = [1, 2]
y_vals = [f.evalf(subs={x: S(i)}) for i in x_vals]
```

```python
error_lagrange(f, np.linspace(I[0], I[1], grado), I)  # Asegurate de que el máximo de la derivada para el grado no es infinito!!!!
```

Este ejercicio lo vamos a resolver de manera aleatoria, usando una simulación. Tenemos que simular el error dados n puntos $x_0$, $x_1$, ..., $x_n$ aleatorios.

```python
max_error = error_maximo_estocastico(f, grado=grado, N=2500)
```

```python
print(1/max_error)
```

```python
x_vals = np.sort(np.random.rand(grado) * (I[1] - I[0]) + I[0])
x_vals = [0, 0.366, 1.333, 2]
y_vals = [f.evalf(subs={x: S(i)}) for i in x_vals]

p, lista_L = polinomio_lagrange(x_vals, y_vals)

x_range = np.linspace(I[0], I[1], 100)
y_real = [f.subs(x, i) for i in x_range]
y_pol = np.array([expand(p).subs(x, i) for i in x_range])

plt.plot(x_range, y_real, label=f'{f}')
plt.scatter(x_vals, y_vals, c="#800000")
plt.plot(x_range, y_pol, label='interpolacion', c="#bcbcbc")
plt.plot(x_range, list(y_pol - (max_error)),  c="#bcbcbc", alpha=0.3)
plt.plot(x_range, list(y_pol + (max_error)),  c="#bcbcbc", alpha=0.3)

# plt.fill_between(list(x_range), list(y_pol - (max_error/2)), list(y_pol + (max_error/2)))

plt.legend()
```

## El fenómeno de Runge y la interpolación de Chebishev
Esta información está en el texto base pero muy sucinta y mal descrita. La info para esta sección la he sacado de https://brianheinold.net/notes/An_Intuitive_Guide_to_Numerical_Methods_Heinold.pdf

El fenómeno de Runge es que cuando tomamos una función la interpolación tiende a fallar en los extremos, sobre todo si los puntos de interpolación los tomamos de manera equidistante.
Obviamente, se pueden escoger los nodos $x_0, x_1, \cdots, x_n$ de muchas maneras, pero unas y otras van a ser mejores para generar interpolaciones más veraces fuera de los extremos.

```python
f = E**(sin(4*x))
I = [0, 1]
grado = 9

x_vals_equip = np.linspace(I[0], I[1], grado)
y_vals_equip = [f.evalf(subs={x: S(i)}) for i in x_vals_equip]

x_vals_extr = [0, 0.03, 0.07, 0.15, 0.5, 0.85, 0.93, 0.97, 1]
y_vals_extr = [f.evalf(subs={x: S(i)}) for i in x_vals_extr]
```

```python
p_equip, lista_L = polinomio_lagrange(x_vals_equip, y_vals_equip)
p_extr, lista_L = polinomio_lagrange(x_vals_extr, y_vals_extr)
```

```python
expand(p_equip)
```

```python
expand(p_extr)
```

```python
fig, axs = plt.subplots(1, 2, figsize=(10, 4))
x_range_base = np.linspace(I[0] - 0.05, I[1] + 0.05, 100)
x_range_todo = np.linspace(I[0] - 1, I[1] + 0.7, 100)

for x_range_idx, x_range in enumerate([x_range_base, x_range_todo]):
    y_real = [f.subs(x, i) for i in x_range]
    y_pol_equip = [expand(p_equip).subs(x, i) for i in x_range]
    y_pol_extr = [expand(p_extr).subs(x, i) for i in x_range]

    axs[x_range_idx].plot(x_range, y_real, label=f'{f}')
    axs[x_range_idx].scatter(x_vals_equip, y_vals_equip, c="#bcbcbc")
    axs[x_range_idx].scatter(x_vals_extr, y_vals_extr, c="#454545")

    axs[x_range_idx].plot(x_range, y_pol_equip, label='interpolacion equiprobable', c="#bcbcbc")
    axs[x_range_idx].plot(x_range, y_pol_extr, label='interpolacion extremos', c="#454545")

    axs[x_range_idx].legend()
```

Vemos que aunque la interpolación equidistante sea más exacta con respecto a la función real, se aleja un montón de los extremos. Sin embargo, si tomamos puntos en el extremo, la interpolación aguanta mejor cambios en el extremo.

Por el teorema del error máximo mencionado anteriormente se deriva que la major elección de valores de $x$ son las raíces de los polinomios de Chevisheb, que se corresponden a la ecuación:
$$x_k = \frac{1}{2}(a+b) + \frac{1}{2}(b - a) \cos\left( \frac{2k - 1}{2n} \pi \right)$$
Para $k = 1, \cdots, n$

```python
help(roots_chebyshev)
```

```python
for n in range(2, 20):
    plt.scatter(roots_chebyshev(n, I=[0, 1]), [n] * n)
```

Repetimos el ejemplo anterior, pero ahora tomando las raíces de Chebyshev

```python
x_vals_chev = [N(i, 6) for i in roots_chebyshev(grado, I=I)]
y_vals_chev = [f.evalf(subs={x: S(i)}) for i in x_vals_chev]
```

```python
p_chev, lista_L = polinomio_lagrange(x_vals_chev, y_vals_chev)
```

```python
fig, axs = plt.subplots(1, 2, figsize=(10, 4))
x_range_base = np.linspace(I[0] - 0.05, I[1] + 0.05, 100)
x_range_todo = np.linspace(I[0] - 1, I[1] + 0.7, 100)

for x_range_idx, x_range in enumerate([x_range_base, x_range_todo]):
    y_real = [f.subs(x, i) for i in x_range]
    y_pol_equip = [expand(p_equip).subs(x, i) for i in x_range]
    y_pol_chev = [expand(p_chev).subs(x, i) for i in x_range]
    y_pol_extr = [expand(p_extr).subs(x, i) for i in x_range]

    axs[x_range_idx].plot(x_range, y_real, label=f'{f}')
    axs[x_range_idx].scatter(x_vals_equip, y_vals_equip, c="#bcbcbc")
    axs[x_range_idx].scatter(x_vals_extr, y_vals_extr, c="#454545")
    axs[x_range_idx].scatter(x_vals_chev, y_vals_chev, c="#800000")

    axs[x_range_idx].plot(x_range, y_pol_equip, label='equiprobable', c="#bcbcbc")
    axs[x_range_idx].plot(x_range, y_pol_extr, label='extremos', c="#454545")
    axs[x_range_idx].plot(x_range, y_pol_chev, label='chebyshev', c="#800000")

    axs[x_range_idx].legend()
```

Aunque los resultados en los extremos sean peor para Chebyshev que para la interpolación de extremos, se ve que en el medio la interpolación de Chebyshev es prácticamente idéntica a la equiprobable. Esto es importante porque para esta función las diferencias en el centro para la interpolación de extremos no son muy evidentes, pero para otras funciones puede que sean peores, y en ese caso la selección de nodos por Chebyshev es ese punto intermedio.

## Algoritmos de Aitken y Neville
Estos dos algoritmos permiten reducir el coste de la computación del polinomio de interpolación. Con estos algoritmos, en lugar de calcular el polinomio de interpolación, se calcula directamente el valor de la función en un punto $x_m$ sabiendo los valores de la función en $n$ puntos diferentes.

```python
help(aitken_neville)
```

```python
# obtenido de https://rpubs.com/aaronsc32/nevilles-method-polynomial-interpolation
N(aitken_neville([8.1, 8.3, 8.6, 8.7], [16.9446, 17.56492, 18.50515, 18.82091], 8.4, modo='neville'))
```

```python
N(aitken_neville([8.1, 8.3, 8.6, 8.7], [16.94, 17.56, 18.5, 18.82], 8.4, modo='aitken'))
```

#### EJERCICIO 49

```python
N(aitken_neville([-2, -1, 0, 1], [-1, -0.5, 0, 0.5], 0.5, modo='neville'))
```

```python
N(aitken_neville([-2, -1, 0, 1], [-1, -0.5, 0, 0.5], 0.5, modo='aitken'))
```

## Interpolación compuesta

#### Figura 5.1

La figura muestra la interpolación de la función de Runge, $f(x) = \frac{1}{1+x^2}$, que vemos falla en los extremos bastante fuertemente.

```python
f = 1 / (1 + x ** 2)
I = [-5, 5]
x_vals = np.linspace(I[0], I[1], 11)
y_vals = [f.evalf(subs={x: S(i)}) for i in x_vals]

p, lista_L = polinomio_lagrange(x_vals, y_vals)

x_range = np.linspace(I[0], I[1], 250)
y_real, y_pol = [f.subs(x, i) for i in x_range], [expand(p).subs(x, i) for i in x_range]

plt.plot(x_range, y_real, label=f'{f}')
plt.scatter(x_vals, y_vals, c="#800000")
plt.plot(x_range, y_pol, label='interpolacion', c="#bcbcbc")

plt.legend()
```

```python
x_vals = [N(i, 6) for i in roots_chebyshev(11, I=I)]
y_vals = [f.evalf(subs={x: S(i)}) for i in x_vals]

p, lista_L = polinomio_lagrange(x_vals, y_vals)

x_range = np.linspace(I[0], I[1], 250)
y_real, y_pol = [f.subs(x, i) for i in x_range], [expand(p).subs(x, i) for i in x_range]

plt.plot(x_range, y_real, label=f'{f}')
plt.scatter(x_vals, y_vals, c="#800000")
plt.plot(x_range, y_pol, label='interpolacion', c="#bcbcbc")

plt.legend()
```

## Interpolación de Hermite

La interpolación de Hermite emplea el método de Newton para construir el polinomio. En este caso se emplean las derivadas de $f$ para aportar más estabilidad y así conseguir un polinomio que sea tanto continuo él como su derivada. En este caso la base es, para $\{x_0, x_1, \cdots, x_n\}$, la siguiente:
$\{\omega_0 = 1, \omega_1 = (x-x_0), \omega_2 = (x-x_0)^2, \omega_3 = (x-x_0)^2(x-x_1), \omega_4 = (x-x_0)^2(x-x_1)^2, \cdots,
\omega_{2n}=(x-x_0)^2(x-x_1)^2\cdots(x-x_n), \omega_{2n+1}=(x-x_0)^2(x-x_1)^2\cdots(x-x_n)^2\}$

En este caso, la variación del método de Newton es que incluimos la información de $f$ y $f'$ en la tabla de diferencias (mostramos para $x_0, x_1, x_2$):

$$
\begin{matrix}
x_0 & f(x_0) &   &  & & &  \\
          &  & f[x_0, x_0] = f'(x_0)  &   & & &\\
x_0 & f(x_0) &                        &  f[x_0, x_0, x_1]  & & &\\
          &  & f[x_0, x_1]  &                              &    f[x_0, x_0, x_1, x_1] & & \\
x_1 & f(x_1) &                        &  f[x_0, x_1, x_1] &                                   & f[x_0, x_0, x_1, x_1, x_2] &\\
          &  & f[x_1, x_1] = f'(x_1)  &                    &    f[x_0, x_1, x_1, x_2]&                                     & f[x_0, x_0, x_1, x_1, x_2, x_2]   \\
x_1 & f(x_1) &                        &  f[x_1, x_1, x_2] &                                   & f[x_0, x_1, x_1, x_2, x_2] &\\
          &  & f[x_1, x_2]  &                              &    f[x_1, x_1, x_2, x_2]  & & \\
x_2 & f(x_2) &                        &  f[x_1, x_2, x_2]& & & \\
          &  & f[x_2, x_2] = f'(x_2)  &   & & &\\
x_2 & f(x_2) &                        &  & & & \\
\end{matrix}
$$

$$p(x) = f(x_0) + f[x_0, x_0] \omega_1 + f[x_0, x_0, x_1] \omega_2 + f[x_0, x_0, x_1, x_1] \omega_3 + f[x_0, x_0, x_1, x_1, x_2] \omega_4 +
    f[x_0, x_0, x_1, x_1, x_2, x_2] \omega_5=$$

$$f(x_0) + f[x_0, x_0] (x-x_0) + f[x_0, x_0, x_1] (x-x_0)^2 + f[x_0, x_0, x_1, x_1] (x-x_0)^2(x-x_1) + f[x_0, x_0, x_1, x_1, x_2] (x-x_0)^2(x-x_1)^2 + f[x_0, x_0, x_1, x_1, x_2, x_2] (x-x_0)^2(x-x_1)^2(x-x_2)$$

La ventaja de esta interpolación es que se pueden hacer las pirámides con tantas derivadas como se quiera. Puedes ver más info en https://en.wikipedia.org/wiki/Hermite_interpolation

```python
help(interpolacion_hermite)
```

#### EJERCICIO 50

```python
p, mat = interpolacion_hermite(x_vals=[1, 2, 5], y_vals = [-1, -3, 3], diff_vals=[-3, -1, 2], var=x)
```

```python
mat
```

```python
p
```

## Método de polinomio genérico
Este método está descrito en el ejercicio 56. Simplemente consiste en desarrollar un polinomio con $n$ incógnitas dando $n$ condiciones que se cumplen de la función o de sus sucesivas derivadas. Por ejemplo, para el ejecicio 56 hay 4 condiciones:
* $f(0) = 1$
* $f'(0.1) = -\frac{10}{3}$
* $f'(0.2) = -\frac{10}{3}$
* $f(0.3) = 0.001$

Por tanto, podemos construir el polinomio $p(x) = a_0 + a_1x + a_2x^2 + a_3x^3$ que satisfaga todas las soluciones, y con eso resolver el sistema.
$$D a = f$$

```python
help(polinomio_generico)
```

#### EJERCICIO 56

```python
p, D, rhs, a_vals = polinomio_generico(lista_condiciones=[[(0, 1), (0.3, 0.001)], [(0.1, -10/3), (0.2, -10/3)]])
p
```

#### EJERCICIO 50

```python
p, D, rhs, a_vals = polinomio_generico(lista_condiciones=[[(1, -1), (2, -3), (5, 3)], [(1, -3), (2, -1), (5, 2)]])
p
```

```python
p.subs(x, 1), p.subs(x, 2), p.subs(x, 5)
```

```python
p.diff(x).subs(x, 1), p.diff(x).subs(x, 2), p.diff(x).subs(x, 5)
```

#### EJERCICIO 51

```python
p, D, rhs, a_vals = polinomio_generico(lista_condiciones=[[(-3, 1), (3, 1)], [(1, 2), (-3, 1)]])
p
```

#### Conclusión

Vamos a extender los límites de este algoritmo con alguna función específica, como la de Runge, y ver si conseguimos alguna interpolación mejor, empleando valores de la función, valores de la primera derivada y valores de la segunda derivada. Para el sampleo de nodos, vamos a tomar los nodos de manera equidistante, y con los nodos del polinomio de Chebyshev.

Por tanto, vamos a tener 6 condiciones: el uso de $f$, $f'$ y $f''$, y nodos equidistantes o por Chebyshev.

```python
I = [-5, 5]
n = 11
f = 1 / (1 + x ** 2)

fdiff = f.diff(x)
fdiff2 = fdiff.diff(x)
```

```python
x_equi = np.linspace(I[0], I[1], n)
x_chev = [N(i, 1) for i in roots_chebyshev(n, I=I)]
```

```python
p_equi_f, D, rhs, a_vals = polinomio_generico(lista_condiciones=[[(i, f.subs(x, i)) for i in x_equi], ])
p_equi_fdiff, D, rhs, a_vals = polinomio_generico(lista_condiciones=[[(i, f.subs(x, i)) for i in x_equi], [(i, fdiff.subs(x, i)) for i in x_equi]])
p_equi_fdiff2, D_equi, rhs, a_vals = polinomio_generico(lista_condiciones=[[(i, f.subs(x, i)) for i in x_equi], [(i, fdiff.subs(x, i)) for i in x_equi],
                                                                      [(i, fdiff2.subs(x, i)) for i in x_equi]])
```

```python
p_chev_f, D, rhs, a_vals = polinomio_generico(lista_condiciones=[[(i, f.subs(x, i)) for i in x_chev], ])
p_chev_fdiff, D, rhs, a_vals = polinomio_generico(lista_condiciones=[[(i, f.subs(x, i)) for i in x_chev], [(i, fdiff.subs(x, i)) for i in x_chev]])
p_chev_fdiff2, D_chev, rhs, a_vals = polinomio_generico(lista_condiciones=[[(i, f.subs(x, i)) for i in x_chev], [(i, fdiff.subs(x, i)) for i in x_chev],
                                                                      [(i, fdiff2.subs(x, i)) for i in x_chev]])
```

Vamos a ver cómo se comportan p, p', y p'' cuando derivamos f. Recordemos que p_equi_f debería cumplir que p = f para los puntos de interés, pero no p = f' o p = f''. Con p_equi_fdiff2, sin embargo, se tiene que cumplir que p = f, p' = f' y p'' = f''.

Empleamos los nodos equidistantes para la visualización porque para la chebyshev las matrices devuelven inversas muy instables y los polinomios no salen correctamente.

```python
fig, axs = plt.subplots(3, 3, figsize=(9, 9))

x_range = np.linspace(I[0], I[1], 250)

list_f = [f, fdiff, fdiff2]
list_p = [p_equi_f, p_equi_fdiff, p_equi_fdiff2]

list_p_label_row = ["p0", "p1", "p2"]
list_p_label_col = ["", "'", "''"]

y_list_label = ["f", "f'", "f''"]

for ax_row in range(3):
    for ax_col in range(3):
        p = list_p[ax_row]
        for _ in range(ax_col):
            p = p.diff(x)

        axs[ax_row][ax_col].scatter(x_equi, [list_f[ax_col].evalf(subs={x: S(i)}) for i in x_equi])
        axs[ax_row][ax_col].plot(x_range, [list_f[ax_col].evalf(subs={x: S(i)}) for i in x_range], label=y_list_label[ax_col], c='#bcbcbc')
        axs[ax_row][ax_col].plot(x_range, [p.evalf(subs={x: S(i)}) for i in x_range],
                                 label=list_p_label_row[ax_row]+list_p_label_col[ax_col], c='#000080')

        min_y, max_y = float(np.min([list_f[ax_col].evalf(subs={x: S(i)}) for i in x_range])), float(np.max([list_f[ax_col].evalf(subs={x: S(i)}) for i in x_range]))
        axs[ax_row][ax_col].set_ylim(bottom=min_y - 0.3 * (max_y - min_y), top=max_y + 0.3 * (max_y - min_y))
        axs[ax_row][ax_col].legend()
plt.tight_layout()
```

Vemos que, a costa de hacer el polinomio más complejo y, a la vez más inestable, conseguimos que las condiciones de la primera y segunda derivadas vayan cumpliéndose. En tal caso, igual conviene tomar menos nodos para evitar tener polinomios tan complejos.

## Método de esplines cúbicos

Los esplines son funciones de interpolación muy útiles porque dan un resultado continuo y doblemente derivable (clase $C^2$) para una serie de puntos $a = x_0 < x_1 < \cdots < x_{n-1} < x_n = b$. Esto es muy util en casos como el de antes, donde un polinomio para varios nodos puede dar un resultado inestable o difícil de computar. Los esplines son $n-1$ polinomios cúbicos que satisfacen la siguiente expresión:

$$S_i(x) = d_i + c_i(x-x_i) + b_i(x-x_i)^2 + a_i(x-x_i)^3$$

Obviamente, cada valor $d_i = f(x_i)$. En total tenemos $3(n-1)$ coeficientes que determinar, por lo que necesitamos un sistema de $3(n-1)$ ecuaciones. Para ello establecemos las siguientes condiciones:
* $S_i(x_{i+1}) = y_{i+1}$, de manera que se satisface la continuidad entre esplines.
* $S_i'(x_{i+1}) = S_{i+1}'(x_{i+1})$, de manera que se satisface que la pendiente es la misma entre dos esplines contiguos, y no hay puntos angulosos entre todos los esplines.
* $S_i''(x_{i+1}) = S_{i+1}''(x_{i+1})$, lo cual garantiza también una continuidad de la segunda derivada.

En este punto nos quedan dos sistemas de ecuaciones para llegar al deseado. Para ello podemos establecer las condiciones del primer y último esplín. En el caso más clásico, el esplín natural, $s_0''(x_0) = x_{n-1}''(x_n) = 0$.

Para determinar los valores $a_i, b_i, c_i$ vamos a resolver el siguiente sistema:
$$
\begin{bmatrix}
r_0 & 2 & 1-r_0 & \cdots & 0 & 0 & 0 \\
0 & r_1 & 2 & \cdots & 0 & 0 & 0 \\
\vdots & \vdots & \vdots & \ddots & \vdots & \vdots & \vdots \\
0 & 0 & 0 & \cdots & 2  & 1-r_{n-3} & 0 \\
0 & 0 & 0 & \cdots & r_{n-2} & 2 & 1-r_{n-2} \\
\end{bmatrix}_{n-2 \times n}
\begin{bmatrix}
z_0\\
z_1\\
\vdots\\
z_n\\
\end{bmatrix}_{n\times 1} =
6\begin{bmatrix}
f[x_0, x_1, x_2]\\
f[x_1, x_2, x_3]\\
\vdots\\
f[x_{n-2}, x_{n-1}, x_n]\\
\end{bmatrix}_{n-2\times 1}
$$

Como el sistema de ecuaciones es indeterminado (hay 2 incógnitas más que ecuaciones), vamos imponer las condiciones de esplín natural, resultando en:

$$
\begin{bmatrix}
2 & 1-r_0 & \cdots & 0 & 0\\
r_1 & 2 & \cdots & 0 & 0\\
\vdots & \vdots & \ddots & \vdots & \vdots\\
0 & 0 & \cdots & 2  & 1-r_{n-3}\\
0 & 0 & \cdots & r_{n-2} & 2\\
\end{bmatrix}_{n-2 \times n-2}
\begin{bmatrix}
z_1\\
\vdots\\
z_{n-1}\\
\end{bmatrix}_{n-2\times 1} =
6\begin{bmatrix}
f[x_0, x_1, x_2]\\
f[x_1, x_2, x_3]\\
\vdots\\
f[x_{n-2}, x_{n-1}, x_n]\\
\end{bmatrix}_{n-2\times 1}
$$

En este sistema, definimos $r_i  = \frac{h_i}{h_i + h_{i+1}}$ donde $h_i = x_{i+1} - x_i$. Los valores $a_i, b_i, c_i$ se obtienen a partir de las siguientes relaciones con $z$:
* $a_i = \frac{z_{i+1} - z_i}{6h_i}$
* $b_i = \frac{z_i}{2}$
* $c_i = \frac{y_{i+1} - y_i}{h_i} - \frac{2z_i + z_{i+1}}{6}h_i$

Y los valores $f[x_i, x_{i+1}, x_{i+2}]$ los obtenemos con el método de Newton.

```python
help(esplines)
```

```python
x_vals, y_vals = [0, 0.25, 0.5, 0.75, 1], [0, 0.7071, 1, 0.7071, 0]
```

```python
S_dict, valores, D, z, rhs = esplines(x_vals, y_vals)
```

```python
valores
```

```python
plt.scatter(x_vals, y_vals)
for i in range(len(x_vals) - 1):
    p = S_dict[f'S_{i}']
    x_range = np.linspace(x_vals[i], x_vals[i+1], 100)
    y_range = [p.subs(x, x_i) for x_i in x_range]

    plt.plot(x_range, y_range)
```

Ahora vamos a aproximar la función de runge por esplines

```python
f = 1 / (1 + x ** 2)
I = [-5, 5]
n = 9
x_vals = np.linspace(I[0], I[1], n)
y_vals = [f.subs(x, xi) for xi in x_vals]
```

```python
S_dict, valores, D, z, rhs = esplines(x_vals, y_vals)
```

```python
plt.scatter(x_vals, y_vals)
plt.plot(np.linspace(I[0], I[1], 250), [f.subs(x, xi) for xi in np.linspace(I[0], I[1], 250)])
for i in range(len(x_vals) - 1):
    p = S_dict[f'S_{i}']
    x_range = np.linspace(x_vals[i], x_vals[i+1], 100)
    y_range = [p.subs(x, x_i) for x_i in x_range]

    plt.plot(x_range, y_range)
```

Vemos que la aproximación por esplines mapea muy bien para valores de 11 en adelante.

## Ejercicios

### Ejercicio 53
Estimar el error que se comete al calcular $e^{\sqrt{x}}$ interpolando el valor de la función en dos puntos $x_0$ y $x_1$ arbitrarios en el intervalo $[1, 2]$.

Recordemos que el error viene dado por

$$E(x) = f(x) - p_n(x) = \frac{(x-x_0)(x-x_1)\cdots(x-x_n)}{(n+1)!}\max_{t\in[a,b]}f^{(n+1)}(t)$$

En este caso, para dos puntos la fórmula queda como:
$$E(x) = \left\vert\frac{(x-x_0)(x-x_1)}{2}\right\vert \left\vert \max_{t\in[1,2]}f''(t) \right\vert$$

Para ello separamos la parte del polinomio y el de la derivada, que son sus dos a maximizar.

La derivada a maximizar es:

```python
f = E ** sqrt(x)
ff = factor(f.diff(x).diff(x))
ff
```

```python
N(ff.subs(x, 2))
```

Como $x\sqrt{x} > x$ para $x > 0$, la función derivada es monótona creciente en el intervalo. Por tanto,
$$\left\vert \max_{t\in[1,2]}f''(t) \right\vert < \left\vert f''(2)  \right\vert < 0.1506$$

```python
x_r = np.linspace(1, 2, 100)
plt.plot(x_r, [ff.subs(x, i) for i in x_r])
```

Por otro lado, para  $\max_{t\in[1,2]}\left\vert(t-x_0)(t-x_1)\right\vert$ tenemos que

$$\max_{t\in[1,2]}\left\vert(t-x_0)(t-x_1)\right\vert =
\max_{t\in[1,2]}\left\vert t^2 - 2t(x_0 + x_1) + x_0x_1\right\vert  \rightarrow_{t = \frac{x_0+x_1}{2}} \rightarrow\left(\frac{x_0+x_1}{2}-x_0\right)\left(x_1 - \frac{x_0+x_1}{2}\right) =
\left(\frac{x_1-x_0}{2}\right)\left(\frac{x_1 - x_0}{2}\right) = \left(\frac{x_1 - x_0}{2}\right)^2 \le \frac{1}{4} $$

Pues la máxima diferencia sucede con $x_1 = 2, x_0 = 1$.

Por tanto, $|E(x)| \le \frac{1}{2}\frac{1}{4} 0.1506 = 0.018825 $

```python
help(polinomio_lagrange)
```

```python
error_lagrange(E**sqrt(x), [Symbol('x_0'), Symbol('x_1')], I=[1,2])  # Asegurate de que el máximo de la derivada para el grado no es infinito!!!!
```

### Ejercicio 55
Construir el polinomio de interpolación de la función de Heaviside en los nodos $[-1, -1/2, -1/3, 0, 1/3, 1/2, 1]$

```python
def heaviside(x):
    if x < 0:
        return 0
    else:
        return 1
```

```python
x_vals = [-S(1), -S(1)/S(2), -S(1)/S(3), 0, S(1)/S(3), S(1)/S(2), S(1)]
y_vals = [heaviside(i) for i in x_vals]
y_vals
```

```python
pol, mat = polinomio_newton(x_vals, y_vals)
expand(pol, frac=True)
```

```python
mat
```

### Ejercicio 56
La función $y=f(x)$ es conocida solamente en los siguientes valores:
$$
f(0)=1, \quad f^{\prime}(0.1)=-\frac{10}{3}, \quad f^{\prime}(0.2)=-\frac{10}{3}, \quad f(0.3)=0.001 .
$$
Esta función tiene un punto de inflexión en el intervalo $0<x<0.3$. Calcular una aproximación de la abscisa de dicho punto usando interpolación mediante polinomios.

```python
p, D, rhs, a_vals = polinomio_generico(lista_condiciones=[[(S(0), S(1)), (S(0.3), S(0.001))], [(S(0.1), -S(10)/S(3)), (S(0.2), -S(10)/S(3))]])
nsimplify(p, tolerance=0.001, full=True)
```

```python
D
```

```python
rhs
```

```python
nsimplify(a_vals, tolerance=0.001, full=True)
```

### Ejercicios 58
Dada la siguiente tabla de valores:
$$
\begin{matrix}
x & 0 & 1 & 2 & 3 & 4 \\
f(x) & 0 & 0 & 2 & 4 & 6
\end{matrix}
$$
calcular el esplin cúbico natural de interpolación a estos datos en el intervalo $[0,1] .$

```python
x_vals, y_vals = [S(i) for i in [0, 1, 2, 3, 4]], [S(i) for i in [0, 0, 2, 4, 6]]
```

```python
S_dict, valores, D, z, rhs = esplines(x_vals, y_vals)
```

```python
S_dict
```

```python
valores
```

```python
D
```

```python
z
```

```python
rhs
```


Basics of rigid body geometry
=============================

This tutorial is an introduction or reminder of the basic elements of rigid body geometry. It covers the definitions of bodies, coordinate systems, coordinates (such as points, vectors and frames), and homogeneous transforms. It follows the nomenclature conventions of Craig, J., 1987. Introduction to robotics: Mechanics and control, in the context of rigid body biomechanics.

While these notions largely come from the robotics field, they will be approached in the scope of rigid body biomechanics. As such, we will use the posture in Figure 1 all through the tutorial as an example for every introduced notion. To facilitate the comprehension, this is a bidimensional example; however, we will treat it as conventional 3D problem, with all z coordinates being set to zero.

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

{height=250px}

Figure 1. The posture used as an example in this tutorial.

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

Coordinates: points, vectors and frames
---------------------------------------

In this section, we will see how to express:

- Points (positions);
- Vectors such as velocities, accelerations and forces; and
- Frames, which are the orientation and position of a coordinate system.

### Global coordinate system ###

To express any coordinate, we need a coordinate system. A coordinate system is composed of an origin (the point in space everything is expressed relative to) and a set of axes. In human movement biomechanics, we usually use a cartesian system composed of three orthonormal axes (x, y and z).

In newton dynamics and at the human scale, it is totally acceptable to define a global, non-moving coordinate system everything can be referenced to. In Figure 2, we define such a fixed system:

- The origin is approximately at the hip level and posterior to the person;
- The x axis points forward;
- The y axis points upward;
- The z axis points to the right.

This coordinate system is completely arbitrary: any other origin or set of orthonormal axes would still be perfectly valid. This is the one we chose here, and the one every global coordinate will refer to.

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

{height=250px}

Figure 2. A global coordinate system

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

### Points and vectors ###

Using the global coordinate system of Figure 2, we can express the position of any point in space using its three components (x, y, z). For example, the position of the shoulder in global coordinates is:

\begin{equation}
~^\text{global}p_\text{shoulder} = \begin{bmatrix}
x_\text{shoulder} \\
y_\text{shoulder} \\
z_\text{shoulder}
\end{bmatrix}
\end{equation}

where $~^\text{global}p_\text{shoulder}$ is read as: Position ($p$) of the shoulder expressed in the global coordinate system.

Note that while three components are sufficient to express points and vectors in three dimensions, we normally use four components instead, the fourth being 1 for points and 0 for vectors:

- **Points**, such as positions, are expressed as $[x, y, z, 1]^T$.
- **Vectors**, such as velocities, accelerations and forces, are expressed as $[x, y, z, 0]^T$.

Therefore, while we express the **position** (a point) of the shoulder in global coordinates as:

\begin{equation}
~^\text{global}p_\text{shoulder} = \begin{bmatrix}
x_\text{shoulder} \\
y_\text{shoulder} \\
z_\text{shoulder} \\ 1
\end{bmatrix}
\end{equation}

we would express its **velocity** (a vector) as:

\begin{equation}
~^\text{global}\vec{v}_\text{shoulder} = \begin{bmatrix}
v_\text{x shoulder} \\
v_\text{y shoulder} \\
v_\text{z shoulder} \\ 0
\end{bmatrix}
\end{equation}

### Local coordinate system ###

As we just saw, points and vectors are easy to express in a coordinate system. The orientation of a segment is however more complex. In Figure 2, if we wanted to express the orientation of the humerus, we would explicitely need this information:

- What is the initial, non-rotated orientation of the humerus?
- By how many degrees is it rotated from its initial orientation?
- Around which axes?

The first step to answer these questions is to create a **local coordinate system** for the humerus. This local coordinate system will be attached to the humerus, and thus will move with it.

To create such a coordinate system, we need to define where is the origin and orthonormal axes of the humerus, in respect to the humerus itself. In this example, we will use the anatomical position as a reference to define this coordinate system (Figure 3):

- The humerus coordinate system's origin is located at the glenohumeral joint;
- Its x axis points forward;
- Its y axis is aligned with the humerus, pointing upward;
- Its z axis points to the right.

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

{height=250px}

Figure 3. Local coordinate system of the humerus.

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

Now that we defined this local coordinate system, we can come back to the position of interest of Figure 1. Look in Figure 4 how the humerus coordinate system is attached to the humerus and thus moves with it.

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

{height=250px}

Figure 4. Expressing the position and orientation of the humerus.

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

### Frames ###

We are now ready to introduce the **frame**, a 4x4 matrix that expresses both the position and the orientation of a coordinate system, in reference to another coordinate system.

The fourth (easiest) column of a frame is the position of the local coordinate system's origin expressed in the reference coordinate system. In the example of Figure 4, this is:

\begin{equation}
~^\text{global}p_\text{humerus} = \begin{bmatrix}
x_\text{humerus} \\
y_\text{humerus} \\
z_\text{humerus} \\ 1
\end{bmatrix}
\end{equation}

The three first columns of a frame express the frame orientation. They express, in the reference coordinate system, the coordinates of three vectors of length 1 that are respectively oriented toward the x, y and z axes of the local coordinate system. Figure 5 illustrates this concept by showing the difference of orientation between the global and the humerus coordinate systems.

{height=250px}

Figure 5. Orientation of the humerus coordinate system.

Based on Figure 5, here is how we would express these three unit vectors (in bold) in both coordinate systems:

|     |  In the humerus coordinate system  |  In the global coordinate system  |
|-----:|:--------------------------------:|:-----------------------------------:|
|  Humerus x axis  |    $\begin{bmatrix} 1 \\ 0 \\ 0 \\ 0 \end{bmatrix}$    |  $\begin{bmatrix} \cos(\theta) \\ \sin(\theta) \\ 0 \\ 0 \end{bmatrix}$  |
|  Humerus y axis  |    $\begin{bmatrix} 0 \\ 1 \\ 0 \\ 0 \end{bmatrix}$    |  $\begin{bmatrix} -\sin(\theta) \\ \cos(\theta) \\ 0 \\ 0 \end{bmatrix}$  |
|  Humerus z axis  |    $\begin{bmatrix} 0 \\ 0 \\ 1 \\ 0 \end{bmatrix}$    |  $\begin{bmatrix} 0 \\ 0 \\ 1 \\ 0 \end{bmatrix}$  |

Combining these four vectors into a single 4x4 matrix gives the frame $~^\text{global}_\text{humerus}T$:

\begin{equation}
~^\text{global}_\text{humerus}T = \begin{bmatrix}
\cos(\theta) & -\sin(\theta) & 0 & x_\text{humerus} \\
\sin(\theta) & \cos(\theta) & 0 & y_\text{humerus} \\
0 & 0 & 1 & z_\text{humerus} \\
0 & 0 & 0 & 1
\end{bmatrix}
\end{equation}

where the expression $~^\text{global}_\text{humerus}T$ is read as: Position and orientation of the humerus coordinate system, expressed in the global coordinate system.

For example, if the shoulder is located 15 cm forward and 70 cm upward to the global origin, and the humerus is inclined at 30 degrees of the vertical, then the position and orientation of the humerus coordinate system is expressed by the frame:

\begin{equation}
~^\text{global}_\text{humerus}T = \begin{bmatrix}
\cos(30) & -\sin(30) & 0 & 0.15 \\
\sin(30) & \cos(30) & 0 & 0.7 \\
0 & 0 & 1 & 0 \\
0 & 0 & 0 & 1
\end{bmatrix} \\=
\begin{bmatrix}
0.866 & -0.5 & 0 & 0.15 \\
0.5 & 0.866 & 0 & 0.7 \\
0 & 0 & 1 & 0 \\
0 & 0 & 0 & 1
\end{bmatrix}
\end{equation}

Independently of the position and orientation of the studied body, a frame always has this form:

\begin{equation}
\begin{bmatrix}
R_{11} & R_{12} & R_{13} & P_x \\
R_{21} & R_{22} & R_{23} & P_y \\
R_{31} & R_{32} & R_{33} & P_z \\
0      & 0      & 0      & 1
\end{bmatrix}
\end{equation}

where:

- the $R$ sub-matrix is a function of three rotation angles and represents the orientation of the local coordinate system;
- the $P$ vector is the position of the local coordinate system's origin.

Rotations and translations: the homogeneous transform
-----------------------------------------------------

The 4x4 matrix that we just constructed is very important, not only to express the position and orientation of a local coordinate system, but also to express transformations such as rotations and translations. In this second case, the same 4x4 matrix is called an homogeneous transform, and:

- the $R$ sub-matrix represents a rotation;
- the $P$ vector is a translation.

### Moving coordinates ###

Any coordinate (point, vector or frame) that is multiplied by the homogeneous transform will be rotated by $R$ and translated by $P$:

\begin{equation}
p_{\text{tranformed}} = T p_\text{initial}
\end{equation}

For example, if we want to rotate the point (10, 0, 0) by 30 degrees around the origin's z axis, then translate it 2 units to the right:

\begin{equation}
p_{\text{tranformed}} =
\begin{bmatrix}
\cos(30) & -\sin(30) & 0 & 2 \\
\sin(30) & \cos(30) & 0 & 0 \\
0 & 0 & 1 & 0 \\
0 & 0 & 0 & 1
\end{bmatrix}
\begin{bmatrix} 10 \\ 0 \\ 0 \\ 1 \end{bmatrix} \\ =
\begin{bmatrix} 10\cos(30) + 2 \\ 10\sin(30) \\ 0 \\ 1 \end{bmatrix} =
\begin{bmatrix} 8.66 \\ 5 \\ 0 \\ 1 \end{bmatrix}
\end{equation}

### Remap coordinates to other coordinate systems ###

An homogenenous transform can rotate and translate any coordinate, including points, vectors and frames. Since the orientation and position of a coordinate system is itself expressed by a frame, then an homogeneous transform can also rotate and translate a coordinate system.

This is a great opportunity to express coordinates from a coordinate system to another. As such, the $~^\text{global}_\text{humerus}T$ matrix that we formed in the last section has a first function that we already know:

1. Expressing the position and orientation of the humerus coordinate system.

But it also has a second function:

2. Remap coordinates from the humerus local coordinate system to the global coordinate system.

This is performed exactly as we did above when we rotated and translated a point. Multiplying a point expressed in a local coordinate system by its own frame of reference gives the position of the point expressed in global coordinates:

\begin{equation}
~^\text{global}p = ~^\text{global}_\text{local}T ~^\text{local}p
\end{equation}

We are due for an example. Let's say we known that the lenght of the arm is 38 cm, and we want to express the position of the elbow in the global coordinate system. We will assume the same posture as before: the shoulder is located 15 cm forward and 70 cm upward to the global origin and the humerus is inclined at 30 degrees of the vertical.

The position of the elbow in the humerus coordinate system is:

\begin{equation}
~^\text{humerus}p_\text{elbow} = \begin{bmatrix}
0 \\ -0.38 \\ 0 \\ 1
\end{bmatrix}
\end{equation}

The humerus frame is:

\begin{equation}
~^\text{global}_\text{humerus}T = \begin{bmatrix}
\cos(30) & -\sin(30) & 0 & 0.15 \\
\sin(30) & \cos(30) & 0 & 0.7 \\
0 & 0 & 1 & 0 \\
0 & 0 & 0 & 1
\end{bmatrix} \\=
\begin{bmatrix}
0.866 & -0.5 & 0 & 0.15 \\
0.5 & 0.866 & 0 & 0.7 \\
0 & 0 & 1 & 0 \\
0 & 0 & 0 & 1
\end{bmatrix}
\end{equation}

Therefore, the position of the elbow in the global coordinate system is:

\begin{equation}
~^\text{global}p_\text{elbow} =
~^\text{global}_\text{humerus}T ~^\text{humerus}p_\text{elbow} \\=
\begin{bmatrix}
0.866 & -0.5 & 0 & 0.15 \\
0.5 & 0.866 & 0 & 0.7 \\
0 & 0 & 1 & 0 \\
0 & 0 & 0 & 1
\end{bmatrix}
\begin{bmatrix}
0 \\ -0.38 \\ 0 \\ 1
\end{bmatrix} \\=
\begin{bmatrix}
0.19 + 0.15 \\
-0.329 + 0.7 \\
0 \\
1 \end{bmatrix} =
\begin{bmatrix}
0.34 \\ 0.371 \\ 0 \\ 1
\end{bmatrix}
\end{equation}

Its final coordinates are $(0.34, 0.371, 0)$.

Main points to remember
-----------------------
We will see in the next tutorial that Kinetics Toolkit's geometry module has several functions to ease the expression of coordinates from one coordinate system to another, to create frames and homogeneous transforms, etc. However, I believe it is important to remember these concepts, to understand how geometric data is represented and calculated.

- A point expresses a position in a given coordinate system, and is written as the 4x1 vector:

\begin{equation}\begin{bmatrix}
x \\ y \\ z \\ 1
\end{bmatrix}\end{equation}

- A vector expresses a displacement, velocity, acceleration, force, etc., in a given coordinate system, and is written as the 4x1 vector:

\begin{equation}\begin{bmatrix}
x \\ y \\ z \\ 0
\end{bmatrix}\end{equation}

- A frame expresses the orientation $R$ and position $P$ of a local coordinate system into a reference coordinate system, and is written as the 4x4 matrix:

\begin{equation}
\begin{bmatrix}
R_{11} & R_{12} & R_{13} & P_x \\
R_{21} & R_{22} & R_{23} & P_y \\
R_{31} & R_{32} & R_{33} & P_z \\
0      & 0      & 0      & 1
\end{bmatrix}
\end{equation}

- An homogeneous transform expresses a rotation $R$ and translation $P$ from a given frame to another, and is written as the same 4x4 matrix:

\begin{equation}
\begin{bmatrix}
R_{11} & R_{12} & R_{13} & P_x \\
R_{21} & R_{22} & R_{23} & P_y \\
R_{31} & R_{32} & R_{33} & P_z \\
0      & 0      & 0      & 1
\end{bmatrix}
\end{equation}


<figure>
  <IMG SRC="gfx/Logo_norsk_pos.png" WIDTH=100 ALIGN="right">
</figure>

# Linear algebra crash course
*Roberto Di Remigio*, *Luca Frediani*

This is a very brief and incomplete introduction to a few selected topics in linear algebra.
An extremely well-written book, covering many topics in mathematics heavily used in physics is James Nearing's [Mathematical Tools for Physics]. The book is freely available online on the author's website.
These notes were translated from the italian version, originally written by Filippo Lipparini and Benedetta Mennucci.

## Vector spaces and bases

#### Definition: Vector space
Let $\mathbb{R}$ be the set of real numbers. Elements of $\mathbb{R}$ will be called _scalars_, to distinguish them from vectors. We define as a vector space $V$ on the field $\mathbb{R}$ the set of vectors such that:

* the sum of two vectors is still a vector. For any pair of vectors $\mathbf{u},\mathbf{v}\in V$ their sum
 $\mathbf{w} = \mathbf{u}+\mathbf{v} = \mathbf{v} + \mathbf{u}$ is still an element of $V$.
* the product of a vector and a scalar is still a vector. For any $\mathbf{v}\in V$ and for any $\alpha \in \mathbb{R}$
 the product $\mathbf{w} = \alpha\mathbf{v}$ is still an element of $V$.

We will write vectors using a boldface font, while we will use normal font for scalars.

Real numbers already are an example of a vector space: the sum of two
real numbers is still a real number, as is their product.
Vectors in the plane are yet another example (when their starting point is the origin of axes):
a sum of two vectors, given by the _parallelogram rule_, is still a vector in the plane; the product of a vector by a scalar is a vector with direction equal to that of the original vector and magnitude given by the product of the scalar and the magnitude
of the original vector.

How big is a vector space? It is possible to get an intuitive idea of dimension for ``geometric'' vector spaces:
the plane has dimension 2, the space has dimension 3, while the set of real numbers has dimension 1.
Some more definitions will help in getting a more precise idea of what the dimension of a vector space is.

#### Definition: Linear combination
A _linear combination_ of vectors $\mathbf{v}_1, \mathbf{v}_2, \ldots, \mathbf{v}_n$ is the vector obtained by summing
these vectors, possibly multiplied by a scalar:
\begin{equation}
 \mathbf{w} = c_1\mathbf{v}_1 + c_2\mathbf{v}_2 + \ldots + c_n\mathbf{v}_n = \sum_{i = 1}^n c_i\mathbf{v}_i
\end{equation}

#### Definition: Linear dependence
Two non-zero vectors are said to be _linearly dependent_ if and only if (iff) there exist two non-zero scalars $c_1, c_2$
 such that:
 \begin{equation}
  c_1\mathbf{v}_1 + c_2\mathbf{v}_2 = \mathbf{0}
 \end{equation}
 where $\mathbf{0}$ is the null vector. Equivalently, two vectors are said to be linearly dependent if one can be written as
 the other multiplied by a scalar:
 \begin{equation}
  \mathbf{v}_2 = -\frac{c_1}{c_2} \mathbf{v}_1
 \end{equation}
Conversely, two vectors are said to be _linearly independent_ when the following is true:
\begin{equation}
 c_1\mathbf{v}_1 + c_2\mathbf{v}_2 = \mathbf{0} \quad\Leftrightarrow\quad c_1=c_2 = 0
\end{equation}

Consider now two linearly dependent vectors in the plane. What does this imply? As one can be expressed as a scalar multiplied by the other,
they have the same _direction_ and _magnitude_ differing by a factor.

The notions above are easily generalized to more than a pair of vectors: vectors $\mathbf{v}_1, \mathbf{v}_2, \ldots\mathbf{v}_n$
are linearly independent iff the only linear combination that gives the null vector is the one where all the coefficients are zero:
\begin{equation}
 \sum_{i=1}^n c_n\mathbf{v}_n = \mathbf{0} \quad\Leftrightarrow\quad c_i = 0 \forall i
\end{equation}

How many linearly independent vectors are there in the plane? As our intuition told us before: only 2! Let us think about the unit vectors
$\mathbf{i}$ and $\mathbf{j}$, i.e. the orthogonal vectors of unit magnitude we can draw on the Cartesian $x$ and $y$ axes. All the vectors in
the plane can be written as a linear combination of those two:
\begin{equation}
 \mathbf{r} = x\mathbf{i} + y\mathbf{j}
\end{equation}
We are already used to specify a vector by means of its _components_ $x$ and $y$. So, by taking linear combinations of the two unit vectors
we are able to generate _all_ the vectors in the plane. Doesn't this look familiar? Think about the set of complex numbers $\mathbb{C}$
and the way we represent it.
It's now time for some remarks:
1. the orthogonal, unit vectors in the plane are linearly independent;
2. they are the minimal number of vectors needed to _span_, i.e. to generate, all the other vectors in the plane.

#### Definition: Basis
 Given the vector space $V$ its _basis_ is defined as the minimal set of linearly independent vectors
 $\mathcal{B} = \lbrace \mathbf{e}_1,\mathbf{e}_2, \ldots,\mathbf{e}_n \rbrace$ spanning all the vectors in the space.
 This means that any suitable linear combination of vectors in $\mathcal{B}$ generates a vector $\mathbf{v}\in V$:
 \begin{equation}
  \mathbf{v} = c_1\mathbf{e}_1 + c_2\mathbf{e}_2 + \ldots + c_n\mathbf{e}_n = \sum_{i=1}^n c_n \mathbf{e}_n
 \end{equation}
 the coefficients in the linear combination are called _components_ or _coordinates_ of the vector $\mathbf{v}$ in the given
 basis $\mathcal{B}$.

Turning back to our vectors in the plane, the set $\lbrace \mathbf{i}, \mathbf{j} \rbrace$ is a basis for the plane and that
that $x$ and $y$ are the components of the vector $\mathbf{r}$.
We are now ready to answer the question: how big is a vector space?

#### Defintion: Dimension of a vector space
 The _dimension_ of a vector space is the number of vectors making up its basis.

[Mathematical Tools for Physics]: http://www.physics.miami.edu/~nearing/mathmethods/

## Vectors using NumPy

We have already seen how to use NumPy to handle quantities that are vector-like. For example, when we needed to plot a function with `matplotlib`, we first generated a _linearly spaced_ vector of $N$ points inside an interval and then evaluated the function in those points.
Vectors, matrices and objects with higher dimensions are all represented as _arrays_.
An array is a _homogeneous_ grid of values that is indexed by a tuple of nonnegative integers.
There are two important terms related to NumPy arrays:
1. the **rank**  of an array is the number of dimensions,
2. the **shape** is a _tuple_ of nonnegative integers, giving the size of the array along each dimension.

Let's see what that means by starting with the simplest array: a one-dimensional array.

```python
import numpy as np
a = np.array([1, 2, 3])  # Create a rank 1 array
print(type(a))            # Prints "<type 'numpy.ndarray'>"
print(a.shape)            # Prints "(3,)"
```

    <class 'numpy.ndarray'>
    (3,)

As you can see, we can easily access all this information. One-dimensional arrays in NumPy can be used to represent vectors, while two-dimensional arrays can be used to represent matrices.
`a = np.array([1, 2, 3])` is equivalent to the vector:
\begin{equation}
  \mathbf{a} = \begin{pmatrix}
                1 \\
                2 \\
                3
               \end{pmatrix}
\end{equation}
The _components_ of the vector can be accessed using so-called **subscript** notation, as in the following code snippet:

```python
print(a[0], a[1], a[2])   # Prints "1 2 3"
a[0] = 5                 # Change an element of the array
print(a)                  # Prints "[5, 2, 3]"
```

    1 2 3
    [5 2 3]

index counting in Python starts from 0 not from 1! As you can see, subscript notation can also be used to modify the value at a given index.
There a number of predefined functions to create arrays with certain properties.
We have already met `linspace` and here is a list of some additional functions:

```python
a = np.zeros(10)  # Create an array of all zeros
print(a)

b = np.ones(10)   # Create an array of all ones
print(b)              # Prints "[[ 1.  1.]]"

c = np.full(10, 7) # Create a constant array
print(c)               # Prints "[[ 7.  7.]
                      #          [ 7.  7.]]"

e = np.random.random(10) # Create an array filled with random values
print(e)
```

    [ 0.  0.  0.  0.  0.  0.  0.  0.  0.  0.]
    [ 1.  1.  1.  1.  1.  1.  1.  1.  1.  1.]
    [7 7 7 7 7 7 7 7 7 7]
    [ 0.73725248  0.27008929  0.14544311  0.93605619  0.67694412  0.48615799
      0.16894559  0.58451987  0.95581802  0.8790677 ]

NumPy supports indexing arrays _via_ **slicing** like Matlab does. While indexing _via_ a single integer number will return the element at that position of the array, _i.e._ a scalar, slicing will return another array with fewer dimensions. For example:

```python
a = np.random.random(15) # Create an array of rank 15
print(a.shape)
print(a)

# Select elements 10, 11 and 15 of the array
print(a[9], a[10], a[14])

# Now take a slice: all elements, but the first and second
print('Taking a slice!')
b = a[2:]
print(b)
# And notice how the rank changed!
print('We have removed two elements!')
print(b.shape)

print('Take another slice: all elements in between the third and tenth')
c = a[2:11]
print(c)
```

    (15,)
    [ 0.88554765  0.67432553  0.31328884  0.78845896  0.79398398  0.22269176
      0.81744839  0.38026416  0.25469862  0.20958785  0.96487864  0.005849
      0.02346493  0.40314848  0.25568283]
    0.209587853612 0.964878644155 0.255682827886
    Taking a slice!
    [ 0.31328884  0.78845896  0.79398398  0.22269176  0.81744839  0.38026416
      0.25469862  0.20958785  0.96487864  0.005849    0.02346493  0.40314848
      0.25568283]
    We have removed two elements!
    (13,)
    Take another slice: all elements in between the third and tenth
    [ 0.31328884  0.78845896  0.79398398  0.22269176  0.81744839  0.38026416
      0.25469862  0.20958785  0.96487864]

As you can see, we can quite easily select just parts of an array and either create new, smaller arrays or just obtain the value at a certain position.
Algebraic operations and functions are also defined on arrays. The definitions for addition and subtraction are obvious. Those for multiplication, division and other mathematical functions are less obvious: in NumPy they are always performed element-wise.
The following example will clarify.

```python
a = np.full(10, 7.)
b = np.full(10, 4.)
print(a)
print(b)

print('Sum of a and b')
print(a + b)

print('Difference of a and b')
print(a - b)

print('Elementwise product of a and b')
print(a * b)

print('Elementwise division of a and b')
print(a / b)

print('Elementwise square root of b')
print(np.sqrt(b))
```

    [ 7.  7.  7.  7.  7.  7.  7.  7.  7.  7.]
    [ 4.  4.  4.  4.  4.  4.  4.  4.  4.  4.]
    Sum of a and b
    [ 11.  11.  11.  11.  11.  11.  11.  11.  11.  11.]
    Difference of a and b
    [ 3.  3.  3.  3.  3.  3.  3.  3.  3.  3.]
    Elementwise product of a and b
    [ 28.  28.  28.  28.  28.  28.  28.  28.  28.  28.]
    Elementwise division of a and b
    [ 1.75  1.75  1.75  1.75  1.75  1.75  1.75  1.75  1.75  1.75]
    Elementwise square root of b
    [ 2.  2.  2.  2.  2.  2.  2.  2.  2.  2.]

```python

```


# Numerical Assignment 1 Dodos

Adapt the python code to model the initial value problem, which describes the Dodo population (p) in the 17th century the year (t=0) that the Dodo are exposed to man and subsequent interactions modeled by the over 10 years using a stepsize of h=0.1.

```python
## Library
import numpy as np
import matplotlib.pyplot as plt
```

## Dodo Population Equation before man
The Dodo population prior to being found by man is described by the non-linear,
\begin{equation}
\frac{d p}{dt}=\alpha_{Birth} p-\alpha_{Death}p^2,
\end{equation}
where $p$ is the population at time $t$,
for $a\leq t \leq b$ years, where  $\alpha_{Birth}$ is the birth rate and $\alpha_{Death}$ is the death rate, with the initial condition
$$ p(a)=A. $$
TIn this simulation we set the parameters of the different equations:
*  as $\alpha_{Birth}=0.3$, and $\alpha_{Death}=0.01$ with the inital condition $p(0)=30$, over the time period $0\leq t \leq 10$.

```python
alpha_birth=0.3
alpha_death=0.01
```

## Discrete Time Domain
To numerically approximate the solution we discretise the continuous domain using a step size, $h=0.1$,
which gives
\begin{equation}
t_i=0+i h,
\end{equation}
for $i=0,...10$.

The Figure below illustrate the discrete domain.

```python
h=0.1
t_end=10
t=np.arange(0,t_end+h/2,h)
fig = plt.figure(1,figsize=(14,4))
ax = fig.add_subplot(1,1,1)
ax.plot(t,0*t,'o:',color='k')
ax.set_xlabel('Time (yrs)')
plt.title('Discrete Time')
plt.show()
```

```python
N=int(t_end/h) # Number of Steps
P=np.zeros(N+1) # Numerical Solution P
P[0]=30
```

We numerically approximate the differential equation using Eulers method to give the difference equation
\begin{equation}
P_{i}=P_{i-1}+h \big(\alpha_{Birth} P_{i-1}-\alpha_{Death}P_{i-1}^2 \big),
\end{equation}
where $h$ is the stepsize and where $P_i$ is the numerical approximation of $p(t_i)$ at distance $t_i$, for $i=0,...,10.$

```python
for i in range (1,N+1):
    P[i]=P[i-1]+h*(alpha_birth*P[i-1]-alpha_death*P[i-1]*P[i-1])

```

## Results
The plot below shows the numerical solution $P_i$ for the Dodo population as a function of time $t$.

```python
## Plotting Figure
fig = plt.figure(figsize=(14,4))
plt.plot(t,P,'o:',color='red',label='Population')
plt.legend()
plt.xlabel('Time (yrs)')
plt.ylabel('Population (thousands)')
#ax.legend(loc='best')
plt.title('Dodo Population, P')
plt.show()
```

## Dodo Population Equation after man
The Dodo population prior to being found by man is described by the non-linear,
\begin{equation}
\frac{d p}{dt}=\alpha_{Birth} p-\alpha_{Death}p^2-p(e^{0.1t}-1),
\end{equation}
where $p$ is the population at time $t$,
for $a\leq t \leq b$ years, where  $\alpha_{Birth}$ is the birth rate and $\alpha_{Death}$ is the death rate, with the initial condition
$$ p(a)=A. $$
The introduction of humans to the island is simulated with the inclusion of the terms
$$-p(e^{0.1t}-1).$$

Like above the parameters are set:
*  as $\alpha_{Birth}=0.3$, and $\alpha_{Death}=0.01$ with the inital condition $p(0)=30$, over the time period $0\leq t \leq 10$.

# Problem 1
Adapt the text in blue and the code below to model the impact of humans on the Dodo population.

We numerically approximate the differential equation using Eulers method to give the difference equation
\begin{equation}
P_{i}=P_{i-1}+h \big(\alpha_{Birth} P_{i-1}-\alpha_{Death}P_{i-1}^2 \ \color{blue}{HUMANS} \big),
\end{equation}
where $h$ is the stepsize and where $P_i$ is the numerical approximation of $p(t_i)$ at distance $t_i$, for $i=0,...,10.$

```python
for i in range (1,N+1):
    ### UPDATED DIFFERENCE FOR DODOS TO INCLUDE HUMANS
    P[i]=P[i-1]+h*(alpha_birth*P[i-1]-alpha_death*P[i-1]*P[i-1])
    ###
```

```python
## Plotting Figure
fig = plt.figure(figsize=(14,4))
plt.plot(t,P,'o:',color='red',label='Population')
plt.legend()
plt.xlabel('Time (yrs)')
plt.ylabel('Population (thousands)')
#ax.legend(loc='best')
plt.title('Dodo Population, P')
plt.show()
```

## Problem 2.

Write a brief description of what the solution of the model tells you about the population of Dodos.

# References

[1] Stover, Christopher and Weisstein, Eric W. "Population Growth." From MathWorld--A Wolfram Web Resource. http://mathworld.wolfram.com/PopulationGrowth.html