 Métodos numéricos

 Segundo cuatrimestre 2021

 Práctica 2: Esquemas temporales

 Cátedra: Pablo Dmitruk

 Fecha límite de entrega: 24 de septiembre de 2021 23:59

### Entrega de:   **COMPLETAR CON SUS NOMBRES**

- [Ejercicios](#ejercicios)

- [Repaso teórico](#explicacion)

# **Ejercicios**

## **Problema 1: Reducción del orden**

Reescriba los siguientes problemas de valores iniciales como un sistema de primer orden.
\begin{alignat}{7}
    \mathrm{a)}& \quad \ddot{y} +  \mu \dot{y} + \omega^2_0 y &&= \cos(t), \qquad \qquad &&y(0) = y_0, \qquad \dot{y}(0) = \dot{y}_0; \\
    \mathrm{b)}& \quad \ddot{y} - \mu(1-t^2) \dot{y} + y &&= 0, \qquad \qquad &&y(0) = y_0, \qquad \dot{y}(0) = \dot{y_0}; \\
    \mathrm{c)}& \quad \dddot{y} + y \ddot{y} + \beta\left(1 - \dot{y}^2 \right) &&= h(t), \qquad \qquad &&y(0) = y_0, \qquad \dot{y}(0) = \dot{y_0}, \qquad \ddot{y}(0) = \ddot{y_0}.
\end{alignat}

**Su resolución acá**

---

## **Problema 2: Derivación de métodos multipaso**
Demuestre la consistencia de los siguientes esquemas de integración temporal y especifique el orden de precisión de cada uno:

$\text{a)}$ Adams-Bashforth de 2 pasos:
\begin{equation*}
    y^{n+1} = y^n + \frac{k}{2} (3 f^n - f^{n-1});
\end{equation*}
$\text{b)}$ Adams-Moulton de 1 paso (de máximo orden):
\begin{equation}
    y^{n+1} = y^n + \frac{k}{2} (f^{n+1} + f^n);
\end{equation}
$\text{c)}$ Adams-Moulton de 2 pasos:
\begin{equation*}
    y^{n+1} = y^n + \frac{k}{12} (5f^{n+1} + 8f^n - f^{n-1}).
\end{equation*}

**Su resolución acá**

---

## **Problema 3: Verificación de orden para métodos multipaso**

Para cada uno de los métodos demostrados en el problema anterior:

$\text{a)}$ integre numéricamente el problema de valor inicial
\begin{equation*}
    \dot{y} = -y + \cos(t), \qquad \qquad y(0) = 3/2,
\end{equation*}
para $0\le t \le 6$ utilizando los pasos temporales $k \in \{1\times 10^{-1},  3\times 10^{-2}, 1 \times 10^{-2}, 3 \times 10^{-3}, 1 \times 10^{-3} \}$. Puede utilizar que este problema admite a $y=e^{-t} + (\mathrm{sen}(t) + \cos(t))/2$ como solución analítica para inicializar los primeros pasos de la integración;

$\text{b)}$ grafique la norma infinito del error en función de $k$ y verifique que encuentra el orden de convergencia esperado.

```python
# Su solución acá
```

---

## **Problema 4: Verificación de orden para métodos predictores correctores**

Repita el estudio realizado en el problema 3 para los siguientes integradores temporales predictores-correctores:

$\text{a)}$ Matsuno;

$\text{b)}$ Heun.

En ambos casos considere solo una iteración del paso corrector.

```python
# Su solución acá
```

---

## **Problema 5: Verificación de orden para Runge-Kutta**

Realice nuevamente lo hecho en los problemas 3 y 4 para los siguientes integradores multietapa (Runge-Kutta):

$a)$ Runge-Kutta de dos etapas punto medio o _midpoint_ (RK2);

$b)$ Runge-Kutta de cuatro etapas (RK4).

```python
# Su solución acá
```

---

## **Problema 6: Integración del oscilador armónico amortiguado**

Integre la ecuación del oscilador amortiguado
\begin{equation*}
    \ddot{x} + \mu \dot{x} + \omega^2_0 x = 0, \qquad \qquad x(0) = x_0, \qquad \dot{x} = \dot{x}_0,
\end{equation*}
para $0\le t \le 5$ utilizando:

$\text{a)}$ el método de Matsuno (con una única iteración del proceso estimación-corrección — EC—);

$\text{b)}$ el método de Runge-Kutta de punto medio (RK2).

Considere en particular $\omega_0 = 2$, $\mu=0,1$, $x_0 = 1$ y $\dot{x}_0 = 0$. Integre el problema para $k \in \{10^{-1}; 10^{-2}; 10^{-3}\}$. Usando la solución analítica $x(t) = e^{-\mu t/2} \bigg(\cos(\tilde{\omega}t) + \mu \mathrm{sen}(\tilde{\omega}t)/2 \tilde \omega\bigg)$, con $\tilde \omega = (4\omega_0^2-\mu^2)^{1/2}/2$, verifique para ambos integradores que el error en la posición final $x(5)$ y la velocidad final $\dot{x}(5)$ decrecen a la tasa esperada al reducir el paso temporal.

```python
# Su solución acá
```

---

## **Problema 7: Estabilidad en la integración de un oscilador**

Considere nuevamente el problema de valores iniciales del ejercicio anterior (i.e. el oscilador armónico amortiguado), pero en esta ocasión tome $\omega_0 = 30$, $\mu=0,5$, $t_f=1$ y los pasos temporales $k \in \{ 8 \times 10^{-2}; 4 \times 10^{-2}; 1 \times 10^{-2} \}$.

Noten que el problema involucra dos escalas temporales bien separadas, una de magnitud $1/30$ ($1/\omega_0$) y otra de magnitud $2$ ($1/\mu$). No es extraño en física toparse con un problema con múltiples escalas, pero donde nos interesa la dinámica asociada a solo una de ellas. Por ejemplo, en este problema, podríamos estar interesados solamente en estudiar la tasa de disipación de energía, para lo cual la escala temporal de $1/30$ no resulta determinante.

Para los parámetros, integre el problema utilizando los siguientes integradores temporales de segundo orden:

$\text{a)}$ Runge-Kutta (punto medio);

$\text{b)}$ Adams-Moulton de segundo orden. _Ayuda: Puede mostrar primeramente que dado el sistema de ecuaciones_
\begin{equation*}
\mathbf{y}^{n+1} = \mathbf{y}^n + \frac{k}{2} \left( A \mathbf{y}^n + A \mathbf{y}^{n+1} \right),
\end{equation*}
puede resolverse como
\begin{equation*}
\mathbf{y}^{n+1} = \tilde{A}^{-1} \left(\mathbf{y}^n + \frac{k}{2} A \mathbf{y}^n\right),
\end{equation*}
_con $\tilde{A} = \mathbb{I} - k A/2$, donde $\mathbb{I}$ es la matriz identidad._

En ambos casos grafique la solución analítica y compárela con la solución obtenida numéricamente. _Ayuda: grafique solo para las ordenadas en el intervalo $(-3/2;\ 3/2)$._

```python
# Su solución acá
```

---

## **Problema 8: Regiones de estabilidad**

Halle analíticamente las regiones de estabilidad para los siguientes métodos predictores-correctores:

$\text{a)}$ Matsuno;

$\text{b)}$ Heun.

Grafique las regiones de estabilidad y compárelas con los métodos de Euler adelantado y Adams-Moulton de 2do orden, respectivamente. _Ayuda: el siguiente código grafica en azul los puntos que satisfacen la desigualdad $| 1 - \bar{\lambda} | < 1$_:
```python
import numpy as np
import matplotlib.pyplot as plt

# Crea una grilla cuadrada de NX x NY puntos para
# (x0,y0) x (x1, y1) y defino una variable compleja sobre ella
Nx, Ny = 1000, 1000
x0, x1 = -2, 2
y0, y1 = -2, 2
x      = np.linspace(x0, x1, Nx)
y      = np.linspace(y0, y1, Ny)
lamda  = x[:,None] + 1j*y[None,:]  # Defino la variable compleja

arg = 1 - lamda    # Defino el argumento del módulo

# Grafico
fig, ax = plt.subplots(1, 1, figsize=(8,4), constrained_layout=True)
ax.imshow((np.abs(arg) < 1).T, extent=[x[0], x[-1], y[0], y[-1]],
          cmap="GnBu", vmin=0, vmax=1);
```

**Su resolución acá**

```python
# Su resolución acá
```

---

## **Problema 9: Verificación numérica de regiones de estabilidad**

Verifique numéricamente las regiones de estabilidad halladas en el problema 8. Para ello puede usar como base la siguiente función que, dado un arreglo 2D $\lambda_{pq} = x_{p} + i y_{q}$ (donde $x$ e $y$ son arreglos de números reales) devuelve un arreglo con `True` donde $\lambda$ es estable y `False` donde no lo es para el esquema de Euler adelantado:
```python
def estabilidad_euler(dt, lamda):
    pasos = 50

    estabilidad = np.ones_like(lamda, dtype=bool)
    for i in range(lamda.shape[0]):
        for j in range(lamda.shape[1]):
            y = 1
            for n in range(0, pasos):
                y = y + dt*(lamda[i,j]*y)

                if np.abs(y) > 2:
                    estabilidad[i,j] = False
                    break

    return estabilidad
```

```python
# Su solución acá
```

---

## **Problema 10: Péndulo (no-lineal)**

La tarea para este ejercicio es escribir dos integradores temporales, de órdenes distintos para la ecuación del péndulo sin la aproximación de pequeñas amplitudes. Consideraremos también el roce con el aire. Este problema queda regido por la ecuación
\begin{equation*}
    \ddot \theta + \mu \dot \theta + \omega_0^2 \mathrm{sen}(\theta) = 0, \qquad \qquad \theta(0) = \theta_0, \qquad \dot \theta(0) = \dot \theta_0, \qquad 0 \le t \le t_f,
\end{equation*}
donde como vimos en la práctica anterior $\omega_0 = \sqrt{g/\ell}$ es la frecuencia natural del sistema ($g$ y $\ell$ representan la aceleración de la gravedad y el largo de la cuerda inextensible desde la cual suspende la masa, respectivamente), y $\mu$ es la magnitud (dimensionada en unidades de frecuencia) del roce con el medio.

Con las herramientas vistas en la práctica podemos resolver esta ecuación ordinaria de segundo orden escribiéndola como un sistema de dos ecuaciones de primer orden:

$\text{a)}$ Escriba la ecuación mencionada como un sistema de ecuaciones de primer orden.

**Su resolución acá**

$\text{b)}$ Escriba dos funciones, `rk2` y `rk4` las cuales, dados $\theta_0$ y $\dot \theta_0$, $\omega_0$, $\mu$, el paso temporal y $t_f$, resuelvan la ecuación del péndulo para los instantes solicitados. Como sus nombres indican `rk2` debera realizar la integración usando un método de Runge-Kutta de 2do orden (punto medio), mientras que `rk4` deberá hacer lo propio utilizando un método de cuarto orden.

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

def rk2(th0, omega0, mu, dt, tf):
    """
    Integra la ecuación del péndulo usando un método de Runge-Kutta
        de segundo orden:

    Entrada:
        - th0: Arreglo de 2 elementos. th[0] es la posición y th[1] la velocidad
        - omega0: frecuencia natural del péndulo.
        - mu: coeficiente de roce del péndulo con el medio circundante.
        - dt: paso temporal a utilizar.
        - tf: tiempo hasta el cual se desea integrar.
    Salida:
        - th: Arreglo de (N, 2), con N la cantidad de pasos temporales
            (incluye la condición inicial). th[:,0] contiene la posición y
            th[:,1] la velocidad.
    """
    # Completar con el integrador RK2
    # ...
    # ...
    # ...

    # Finalmente, convierto theta al intervalo [-pi,pi)
    th[:, 0] = np.arctan2(np.sin(th[:,0]), np.cos(th[:,0]))
    return th

def rk4(th0, omega0, mu, dt, tf):
    """
    Integra la ecuación del péndulo usando un método de Runge-Kutta
        de cuarto orden.

    Entrada:
        - th0: Arreglo de 2 elementos. th[0] es la posición y th[1] la velocidad
        - omega0: frecuencia natural del péndulo.
        - mu: coeficiente de roce del péndulo con el medio circundante.
        - dt: paso temporal a utilizar.
        - tf: tiempo hasta el cual se desea integrar.
    Salida:
        - th: Arreglo de (N, 2), con N la cantidad de pasos temporales
            (incluye la condición inicial). th[:,0] contiene la posición y
            th[:,1] la velocidad.
    """
    # Completar con el integrador RK4
    # ...
    # ...
    # ...

    # Finalmente, convierto theta al intervalo [-pi,pi)
    th[:, 0] = np.arctan2(np.sin(th[:,0]), np.cos(th[:,0]))
    return th

#                  NO EDITAR DEBAJO DE ESTA LÍNEA
#-------------------------------------------------------------------------------
def estetizar_graficos(ax, titulo, etiqueta_x, etiqueta_y):
    """
        Dado el par de ejes `ax` coloca el título y las etiquetas de los ejes
        x e y. Además de existir crea también las leyendas y agregra una grilla.
    """
    ax.legend()
    ax.grid()
    ax.set_title(titulo)
    ax.set_xlabel(etiqueta_x)
    ax.set_ylabel(etiqueta_y)
```

### Posición y velocidad sin roce
A diferencia del oscilador resuelto en la práctica anterior, no contamos en esta ocasión con una solución analítica (al menos en términos de funciones elementales) contra la cual contrastar para verficar el funcionamiento de nuestra solución.

En estos casos, una manera de comprobar si nuestro _solver_ se comporta de manera razonable, es analizar las soluciones que devuelve para casos límites, situaciones en las que podemos aprovechar nuestro conocimiento físico del problema.

$\text{c)}$ Piense cualitativamente como debería ser el movimiento en los siguientes casos:

 $\theta_0 = 0,1$, $\dot \theta_0 = 0$;

 $\theta_0 = \pi - 0,1$, $\dot \theta_0 = 0$;

 $\theta_0 = \pi$, $\dot \theta_0 = 0$;

 $\theta_0 = 0$, $\dot \theta_0 = 2\omega + 0,01$ ($2\omega$ es la máxima diferencia de energía potencial gravitatoria entre dos puntos cualesquiera).

Luego, para ambos métodos de Runge-Kutta, integre la ecuación para cada condición inicial fijando $\omega_0 = 2$, $\mu=0$, $k=10^{-3}$ y $t_f = 10$. Grafique la posición y la velocidad obtenidas. En todos los casos verifique que recupera el comportamiento esperado.

```python
# Posición y velocidad sin roce
#-------------------------------
# Figura y pares de ejes
fig, axs = plt.subplots(2, 4, figsize=(16,4), constrained_layout=True)

dt =    # COMPLETAR: Paso temporal

# Integrar para el primer conjunto de condiciones iniciales
th_2 =  # COMPLETAR: guardar en th_2 el resultado de integrar con RK2
th_4 =  # COMPLETAR: guardar en th_2 el resultado de integrar con RK4

#-------------------------------------------------------------------------------
#                            NO EDITAR ESTA SECCIÓN
#-------------------------------------------------------------------------------
t = np.arange(th_2.shape[0])*dt
axs[0,0].plot(t, th_2[:,0], label="RK2")
axs[0,0].plot(t, th_4[:,0], label="RK4")
axs[1,0].plot(t, th_2[:,1], label="RK2")
axs[1,0].plot(t, th_4[:,1], label="RK4")
estetizar_graficos(axs[0,0], r"$\theta_0 = 0,1 \ \wedge \ \dot \theta_0 = 0$",
                  "$t$", r"$\theta$")
estetizar_graficos(axs[1,0], "", "$t$", r"$\dot \theta$")
#-------------------------------------------------------------------------------

# Integrar para el segundo conjunto de condiciones iniciales
th_2 =  # COMPLETAR: guardar en th_2 el resultado de integrar con RK2
th_4 =  # COMPLETAR: guardar en th_2 el resultado de integrar con RK4

#-------------------------------------------------------------------------------
#                            NO EDITAR ESTA SECCIÓN
#-------------------------------------------------------------------------------
t = np.arange(th_2.shape[0])*dt
axs[0,1].plot(t, th_2[:,0], label="RK2")
axs[0,1].plot(t, th_4[:,0], label="RK4")
axs[1,1].plot(t, th_2[:,1], label="RK2")
axs[1,1].plot(t, th_4[:,1], label="RK4")
estetizar_graficos(axs[0,1], r"$\theta_0 = \pi-0,1 \ \wedge \ \dot \theta_0=0$",
                  "$t$", r"$\theta$")
estetizar_graficos(axs[1,1], "", "$t$", r"$\dot \theta$")
#-------------------------------------------------------------------------------

# Integrar para el tercer conjunto de condiciones iniciales
th_2 =  # COMPLETAR: guardar en th_2 el resultado de integrar con RK2
th_4 =  # COMPLETAR: guardar en th_2 el resultado de integrar con RK4

#-------------------------------------------------------------------------------
#                            NO EDITAR ESTA SECCIÓN
#-------------------------------------------------------------------------------
t = np.arange(th_2.shape[0])*dt
axs[0,2].plot(t, th_2[:,0], label="RK2")
axs[0,2].plot(t, th_4[:,0], label="RK4")
axs[1,2].plot(t, th_2[:,1], label="RK2")
axs[1,2].plot(t, th_4[:,1], label="RK4")
estetizar_graficos(axs[0,2], r"$\theta_0 = \pi \ \wedge \ \dot \theta_0 = 0$",
                  "$t$", r"$\theta$")
estetizar_graficos(axs[1,2], "", "$t$", r"$\dot \theta$")
#-------------------------------------------------------------------------------

# Integrar para el cuarto conjunto de condiciones iniciales
th_2 =  # COMPLETAR: guardar en th_2 el resultado de integrar con RK2
th_4 =  # COMPLETAR: guardar en th_2 el resultado de integrar con RK4

#-------------------------------------------------------------------------------
#                            NO EDITAR ESTA SECCIÓN
#-------------------------------------------------------------------------------
t = np.arange(th_2.shape[0])*dt
axs[0,3].plot(t, th_2[:,0], label="RK2")
axs[0,3].plot(t, th_4[:,0], label="RK4")
axs[1,3].plot(t, th_2[:,1], label="RK2")
axs[1,3].plot(t, th_4[:,1], label="RK4")
estetizar_graficos(axs[0,3], r"$\theta_0 = 0 \ \wedge \ " +\
                  r"\dot \theta_0 = 2\omega_0 + 0,01$", "$t$", r"$\theta$")
estetizar_graficos(axs[1,3], "", "$t$", r"$\dot \theta$");
```

### Conservación de la energía
Además de verificar que el movimiento, cualitativamente, es consistente con nuestro conocimiento físico, para el caso sin roce sabemos que debe conservarse la energía mecánica, que podemos definir como
\begin{equation*}
    E(t) = \dot \theta(t)^2 + 4 \omega_0^2 \mathrm{sen}^2\left(\frac{\theta(t)}{2} \right).
\end{equation*}

$\text{d)}$ Escriba una función `energia` que reciba como valores de entrada a $\theta$, $\dot \theta$ y $\omega_0$ y devuelva la energía mecánica para cada instante. Usando esta función verifique que a todo tiempo se verifica aproximadamente la conservación de la energía, es decir, $|\Delta E| = |E(t) - E(0)| \ll 1$, para todos los casos estudiados en el inciso anterior, considerando esta vez $t_f=75$.

```python
# Estudio de conservación de energía para el caso sin rozamiento

def energia(th, omega):
    """
        Calcula la energía mecánica para todo tiempo.

    Entrada:
        - th: Arreglo de forma (N,2) con la posición ([:,0]) y
            la velocidad  angular ([:,1]).
        - omega_0: Frecuencia natural del sistema.

    Salida:
        - energia: Arreglo de forma (N).
    """
    # COMPLETAR

    return energia

# Figuras, pares de ejes y paso temporal
fig, axs = plt.subplots(1, 4, figsize=(16,4), constrained_layout=True)
dt       = 1e-3
offset   = int(2/1e-3)

# COMPLETAR: Calcular la variación de energía para todo instante usando RK2
# para el primer conjunto de condiciones iniciales y asignarla a la variable
# DeltaE_2.

# COMPLETAR: Calcular la variación de energía para todo instante usando RK4
# para el primer conjunto de condiciones iniciales y asignarla a la variable
# DeltaE_4.

# ----------------------------- NO EDITAR --------------------------------------
t = np.arange(0, DeltaE_2.shape[0])*dt
axs[0].semilogy(t[offset:], DeltaE_2[offset:], label="RK2")
axs[0].semilogy(t[offset:], DeltaE_4[offset:], label="RK4")
estetizar_graficos(axs[0], r"$\theta_0 = 0,1 \ \wedge \ \dot{\theta}_0 = 0$",
                  "$t$", r"$|\Delta E|$")
# ------------------------------------------------------------------------------

# COMPLETAR: Calcular la variación de energía para todo instante usando RK2
# para el segundo conjunto de condiciones iniciales y asignarla a la variable
# DeltaE_2.

# COMPLETAR: Calcular la variación de energía para todo instante usando RK4
# para el segundo conjunto de condiciones iniciales y asignarla a la variable
# DeltaE_4.

# ----------------------------- NO EDITAR --------------------------------------
t = np.arange(0, DeltaE_2.shape[0])*dt
axs[1].semilogy(t[offset:], DeltaE_2[offset:], label="RK2")
axs[1].semilogy(t[offset:], DeltaE_4[offset:], label="RK4")
estetizar_graficos(axs[1], r"$\theta_0 = \pi-0,1 \ \wedge \ \dot{\theta}_0 = 0$",
                  "$t$", r"$|\Delta E|$")
# ------------------------------------------------------------------------------

# COMPLETAR: Calcular la variación de energía para todo instante usando RK2
# para el tercer conjunto de condiciones iniciales y asignarla a la variable
# DeltaE_2.

# COMPLETAR: Calcular la variación de energía para todo instante usando RK4
# para el tercer conjunto de condiciones iniciales y asignarla a la variable
# DeltaE_4.

# ----------------------------- NO EDITAR --------------------------------------
t = np.arange(0, DeltaE_2.shape[0])*dt
axs[2].semilogy(t[offset:], DeltaE_2[offset:], label="RK2")
axs[2].semilogy(t[offset:], DeltaE_4[offset:], label="RK4")
estetizar_graficos(axs[2], r"$\theta_0 = \pi \ \wedge \ \dot{\theta}_0 = 0$",
                  "$t$", r"$|\Delta E|$")
# ------------------------------------------------------------------------------

# COMPLETAR: Calcular la variación de energía para todo instante usando RK2
# para el cuarto conjunto de condiciones iniciales y asignarla a la variable
# DeltaE_2.

# COMPLETAR: Calcular la variación de energía para todo instante usando RK4
# para el cuarto conjunto de condiciones iniciales y asignarla a la variable
# DeltaE_4.

# ----------------------------- NO EDITAR --------------------------------------
t = np.arange(0, DeltaE_2.shape[0])*dt
axs[3].semilogy(t[offset:], DeltaE_2[offset:], label="RK2")
axs[3].semilogy(t[offset:], DeltaE_4[offset:], label="RK4")
estetizar_graficos(axs[3], rr"$\theta_0 = 0 \ \wedge \ " +\
                  r"\dot \theta_0 = 2\omega_0 + 0,01$", "$t$", r"$|\Delta E|$");
# ------------------------------------------------------------------------------
```

### Reproducción del período real
También usando la conservación de la energía, y si el movimiento parte del reposo, podemos obtener una expresión analítica para el período del movimiento:
\begin{equation*}
    T = \frac{4}{\omega} K\left( \mathrm{sen}^2\left( \frac{\theta_0}{2}\right)\right),
\end{equation*}
donde $\theta_0$ es la posición inicial y $K$ la integral elíptica completa de primera especie.

$\text{e)}$ Para cada integrador temporal, calcule la diferencia entre el período dado por la expresión analítica y el que obtiene a partir de sus simulaciones para el conjunto de posiciones iniciales $[\boldsymbol \theta_0]_j = 0,01 + j\Delta \theta_0$, $0 \le j < 20$ y $\Delta \theta_0 = 0,1234$ (es decir 20 puntos equiespaciados entre $0,01$ y $3\pi/4$). En todos los casos considere nula a la velocidad angular inicial. Analice en particular los siguientes casos:

$ \omega_0 = 1, \ \mu=0, \ k=1 \times 10^{-2},\ t_f = 250 $;

$ \omega_0 = 10, \ \mu=0, \ k=1 \times 10^{-2},\ t_f = 250 $;

$ \omega_0 = 10, \ \mu=0, \ k=1 \times 10^{-3},\ t_f = 250 $.

Como ayuda le proporcionamos funciones que calculan ambos períodos.

```python
def periodo_analitico(th0, omega0):
    """
    Calcula el período del péndulo sin roce dada la posición inicial y la
    frecuencia natural del sistema.

    Entradas:
        - th0: Número de punto flotante con la posición inicial de la masa.
        - omega0: Frecuencia natural del sistema.

    Salida:
        - Período del péndulo.
    """
    import scipy.special as spspecial
    return 4/omega0*spspecial.ellipk(np.sin(th0/2)**2)

def periodo_datos(th, dt):
    """
    Calcula el período del péndulo sin roce dada una serie de posiciones
    theta(t) y el paso temporal entre muestras.

    Entradas:
        - th: Vector de N elementos con la posición de la masa para N instantes
            consecutivos.
        - dt: Espaciamemiento entre muestras.

    Salida:
        - Período del péndulo.
    """
    import scipy.signal as spsignal

    # Uso una ventana de Blackman-Harris para reducir Gibbs
    con_ventana = th*spsignal.blackmanharris(th.size)

    # Relleno con ceros para interpolar el espectro
    ceros    = 10000
    T_previo = 0
    T_actual = 1

    # Pruebo distintos niveles de relleno hasta converger 3 dígitos decimales
    while np.abs((T_previo-T_actual)/T_actual) > 1e-3:
        ceros   = 2*ceros
        relleno = np.pad(con_ventana, ceros, mode="constant")
        RELLENO = np.fft.rfft(relleno)
        f       = np.fft.rfftfreq(relleno.size, d=dt)

        # Busco el pico de la FFT y saco la frecuencia del pico. Transformo a T.
        ind      = np.argmax(np.abs(RELLENO))
        T_previo = T_actual
        T_actual = 1/f[ind]

    return T_actual

# Figura y pares de ejes
fig, axs = plt.subplots(1, 3, figsize=(12,4), constrained_layout=True)

# Vector de tiempos
dt, tf = 1e-2, 250
pasos  = int(round(tf/dt))
t      = np.arange(0, pasos+1)*dt

# Conjunto de condiciones iniciales y diferencias de periodos
N = 20
conds_ini = np.linspace(1e-2, 3*np.pi/4, N)
DeltaT_2  = np.zeros(N)
DeltaT_4  = np.zeros(N)

# Períodos para omega_0 = 1
for n, ini in enumerate(conds_ini):
    # COMPLETAR: Obtener la diferencia de períodos para la condicion inicial
    # actual y guardarlos en DeltaT_2[n] (para RK2) y DeltaT_4[n] (para RK4).

# -------------------- NO EDITAR ESTA SECCIÓN ----------------------------------
axs[0].plot(conds_ini, DeltaT_2, "x", c="C0", label="RK2", markersize=10)
axs[0].plot(conds_ini, DeltaT_4, "o", c="C1", label="RK4")
estetizar_graficos(axs[0], r"$\omega_0=1, \ \Delta t=1\times 10^{-2}$",
                  r"$\theta_0$", "$T$")
# ------------------------------------------------------------------------------

# Períodos para omega_0 = 10
for n, ini in enumerate(conds_ini):
    # COMPLETAR: Obtener la diferencia de períodos para la condicion inicial
    # actual y guardarlos en DeltaT_2[n] (para RK2) y DeltaT_4[n] (para RK4).

# -------------------- NO EDITAR ESTA SECCIÓN ----------------------------------
axs[1].plot(conds_ini, DeltaT_2, "x", c="C0", label="RK2", markersize=10)
axs[1].plot(conds_ini, DeltaT_4, "o", c="C1", label="RK4")
estetizar_graficos(axs[1], r"$\omega_0=10, \ \Delta t=1\times 10^{-2}$",
                  r"$\theta_0$", "$T$")
# ------------------------------------------------------------------------------

# Vector de tiempos
dt, tf = 1e-3, 250
pasos  = int(round(tf/dt))
t = np.arange(0, pasos+1)*dt

# Períodos para omega0 = 10 con k más chico
for n, ini in enumerate(conds_ini):
    # COMPLETAR: Obtener la diferencia de períodos para la condicion inicial
    # actual y guardarlos en DeltaT_2[n] (para RK2) y DeltaT_4[n] (para RK4).

# -------------------- NO EDITAR ESTA SECCIÓN ----------------------------------
axs[2].plot(conds_ini, DeltaT_2, "x", c="C0", label="RK2", markersize=10)
axs[2].plot(conds_ini, DeltaT_4, "o", c="C1", label="RK4")
estetizar_graficos(axs[2], r"$\omega_0=10, \ \Delta t=1 \times 10^{-3}$",
                  r"$\theta_0$", "$T$");
# ------------------------------------------------------------------------------
```

**Nota**: La celda anterior puede tardar entre 5 y 10 minutos en ejecutarse. Intente reducir la cantidad de condiciones iniciales `N` a 2 o 3 hasta estar seguro de estar obteniendo resultados razonables.

### Posición y velocidad con roce

Obtenidos resultados satisfactorios en los incisos previos, pueden tener ya una cierta confianza en que los integradores que escribieron evolucionan aceptablemente el término no lineal. Resta realizar algunas pruebas para ver que los resultados para los casos con roce también se ajustan a lo esperado.

$\text{f)}$ Considerando $\theta_0=0,01$, $\omega_0 = 1$, $k=10^{-2}$, $t_f=100$ y para ambos integradores temporales, integre en tiempo los casos:

$\mu = 0,1$ (amortiguamiento subcrítico);

$\mu = 10$ (amortiguamiento supercrítico).

Verifique que, cualitativamente, la posición y la velocidad angular presentan el comportamientos esperados.

```python
# Figura, pares de ejes y parámetros temporales
fig, axs = plt.subplots(2, 2, figsize=(8,8), constrained_layout=True)
dt, tf = 1e-2, 100

# COMPLETAR: Con las soluciones para RK2 (th_2) y RK4 (th_4) en el caso
# mu = 0,1.
th_2  = #...
th_4  = #...

# --------------------------- NO EDITAR ESTA SECCIÓN ---------------------------
t = np.arange(0, th_2.shape[0])*dt
axs[0,0].plot(t, th_2[:,0], label="RK2")
axs[0,0].plot(t, th_4[:,0], label="RK4")
estetizar_graficos(axs[0,0], "Amortiguamiento subcrítico", "$t$", r"$\theta$")
axs[1,0].plot(t, th_2[:,1], label="RK2")
axs[1,0].plot(t, th_4[:,1], label="RK4")
estetizar_graficos(axs[1,0], "", "$t$", r"$\dot\theta$")
# ------------------------------------------------------------------------------

# COMPLETAR: Con las soluciones para RK2 (th_2) y RK4 (th_4) en el caso
# mu = 10
th_2  = #...
th_4  = #...

# --------------------------- NO EDITAR ESTA SECCIÓN ---------------------------
t = np.arange(0, th_2.shape[0])*dt
axs[0,1].plot(t, th_2[:,0], label="RK2")
axs[0,1].plot(t, th_4[:,0], label="RK4")
estetizar_graficos(axs[0,1], "Amortiguamiento supercrítico", "$t$", r"$\theta$")
axs[1,1].plot(t, th_2[:,1], label="RK2")
axs[1,1].plot(t, th_4[:,1], label="RK4")
estetizar_graficos(axs[1,1], "", "$t$", r"$\dot\theta$");
# ------------------------------------------------------------------------------
```

### Balance entre pérdida de energía mecánica y potencia disipada

En términos de energía mecánica ya no vamos a tener conservación para el caso con disipación. Sin embargo, resulta sencillo derivar la siguiente ecuación de balance
\begin{equation*}
    \dot E (t) = - \mu \dot \theta^2(t) = P_\mu(t),
\end{equation*}
donde $P_\mu$ es la potencia disipada por la fuerza de roce.

$\text{g)}$ Para los casos estudiados en el inciso anterior, verifique si se verifica (de manera aproximada) la ecuación de balance mencionada. Para ello, grafique la cantidad $|\dot{E} - P_\mu|$ en función del tiempo.

```python
# Balance variación de energía - disipación
def potencia_roce(th_punto, mu):
    """
    Calcula la potencia entregada/disipada al sistema por el roce con el aire.

    Entrada:
        - th_punto: Arreglo de (N) elementos con la velocidad angular de la
            masa para N instantes de tiempo.
        - mu: Coeficiente de la fuerza de rozamiento (en unidades de frecuencia)

    Salida:
        - potencia: Arreglo de (N) elementos con la potencia entregada/disipada
            por la fuerza de roce a cada instante.
    """
    return # COMPLETAR

def variacion_energia(energia, dt):
    """
    Calcula la variación temporal de la energía mecánica usando diferencias
    finitas centradas de 6to orden. En los extremos del intervalo, donde no se
    tienen suficientes datos, se devuelve 0.

    Entrada:
        - energia: Arreglo de (N) elementos con la energía mecánica para N
            N instantes consecutivos.
        - dt: Espaciamiento entre muestras de la energía mecánica.

    Salida:
        - var: Arreglo de (N) elementos con la variación temporal de la energía
            mecánica.
    """
    var         = np.zeros_like(energia)
    var         = -1*np.roll(energia, 3)  + 9*np.roll(energia, 2)
    var        += -45*np.roll(energia, 1) + 45*np.roll(energia, -1)
    var        += -9*np.roll(energia, -2) + np.roll(energia, -3)
    var         = var/(60*dt)
    var[-3:]    = 0
    var[:3]     = 0
    return var

# Figuras, pares de ejes y paso temporal
fig, axs = plt.subplots(1, 2, figsize=(8,4), constrained_layout=True)
dt     = 1e-2
offset = int(round(2/dt))

# Integración para mu = 0,1

# COMPLETAR: Calcular el valor absoluto de la diferencia entre la variación
# de energía y la potencia disipada. Guarde sus resultados en las variables
# bal_2 (para la integración de segundo orden) y bal_4 (para la de cuarto).

# --------------------------- NO EDITAR ESTA SECCIÓN ---------------------------
t = np.arange(bal_2.shape[0])*dt
axs[0].semilogy(t[offset:], bal_2[offset:], label="RK2")
axs[0].semilogy(t[offset:], bal_4[offset:], label="RK4")
estetizar_graficos(axs[0], "Amortiguamiento subcrítico",
                  "$t$", r"$|\partial_t E - P_\mu|$")
# ------------------------------------------------------------------------------

# Integración para mu = 10

# COMPLETAR: Calcular el valor absoluto de la diferencia entre la variación
# de energía y la potencia disipada. Guarde sus resultados en las variables
# bal_2 (para la integración de segundo orden) y bal_4 (para la de cuarto).

# --------------------------- NO EDITAR ESTA SECCIÓN ---------------------------
t = np.arange(bal_2.shape[0])*dt
axs[1].semilogy(t[offset:], bal_2[offset:], label="RK2")
axs[1].semilogy(t[offset:], bal_4[offset:], label="RK4")
estetizar_graficos(axs[1], "Amortiguamiento supercrítico",
                  "$t$", r"$|\partial_t E - P_\mu|$")
# ------------------------------------------------------------------------------
```

### Análisis de resultados

$\text{h)}$ Describa **brevemente** los resultados obtenidos en los incisos anteriores.

**Su resolución acá**

---

---

# **Esquemas temporales**

## **Integración temporal**

En esta práctica nos interesa poder resolver numéricamente el problema
\begin{align*}
    \dot y &= f(t, y),\\
    y(t_0)   &= y_0,
\end{align*}
donde para alivianar la notación usaremos en adelante $\dot y = \mathrm{d}y/\mathrm{d}t$.

Noten que, a diferencia de lo que vimos en la práctica anterior, conocemos ahora la derivada y queremos recuperar $y^n$ (es decir, el valor de $y$ sobre una cantidad discreta de valores de $t$, $t^n$). Luego, asumiendo conocidos $y^j, f(t^j, y^j)$ para $0 \le j \le n$, una forma de determinar $y^{n+1}$ es usar expansiones de Taylor. Por ejemplo, expandiendo $y^{n+1}$ alrededor de $y^n$ o bien $y^n$ alrededor de $y^{n+1}$, tenemos respectivamente
\begin{alignat*}{5}
    y^{n+1} &= y^n     &+&     \dot y^n k &+& \mathcal{O}(k^2),\\
    y^n     &= y^{n+1} &-& \dot y^{n+1} k &+& \mathcal{O}(k^2),
\end{alignat*}
donde $k = \Delta t$ es el paso temporal y $f^n = f(t^n, y^n)$. Podemos despejar fácilmente sendos métodos de segundo **orden local**
\begin{align}
    y^{n+1} &= y^n + f^n k, \tag{Euler adelantado}\\
    y^{n+1} &= y^n + f^{n+1}k. \tag{Euler atrasado}
\end{align}
Como indican las etiquetas, estos métodos reciben el nombre de **Euler adelantado** y **Euler atrasado**, respectivamente. Estos métodos determinan $y^{n+1}$ a segundo orden, si conocemos $y^n$ y $f^n$ o $f^{n+1}$ de manera exacta (o, al menos, a orden $k^2$ y $k$, respectivamente). Sin embargo, este no suele ser el caso, ya que lo usual suele ser partir de una condición inicial y aplicar integradores temporales de forma iterativa hasta llegar a la solución a un cierto tiempo final $t_f$ deseado. Al aplicar el método $N=t_f/k$ veces, el orden será entonces $N \mathcal{O}(k^2) = t_f \mathcal{O}(k)$. En consecuencia, el **orden global**, es decir, el error asociado a integrar hasta un cierto tiempo $t$, es $\mathcal{O}(k)$ para los métodos de Euler (adelantado y atrasado). Encontraremos esta diferencia de un orden de magnitud entre el orden local y el orden global para todos los esquemas temporales que veremos, al menos mientras permanezcan **estables**, concepto que discutiremos más adelante. Excepto mención explícita de lo contrario, **cuando nos referimos al orden de un integrador temporal hacemos referencia al orden global** del mismo.

Un elemento en el que aún no reparamos es en que si conocemos funcionalmente $f$, y contamos con $y^n$, la ecuación de Euler hacia adelante resulta inmediata para resolver. Sin embargo, la versión atrasada, es una expresión implícita para $y^{n+1}$ (ya que $f^{n+1}$ depende de $y^{n+1}$). Esto último no presenta gran dificultad (en precisión infinita) si $f^{n+1}$ es lineal, pero no resulta trivial en casos generales, requiriendo generalmente usar métodos iterativos para hallar $y^{n+1}$. A los métodos que, conocida toda la información para $t^j, y^j$, $j\le n$ permiten determinar explícitamente $y^{n+1}$ se los llama **métodos explícitos**. Por otro lado, aquellos que requiren resolver una ecuación implicita (como Euler hacia atrás), se los llama **métodos implícitos**. A la hora de utilizar métodos implícitos, es usual conjugarlos con algún esquema iterativo, como por ejemplo el algoritmo de Newton-Raphson hasta alcanzar la tolerancia deseada. Así planteado, parecería que los métodos implícitos son una complicación innecesaria, sin embargo a la hora de discutir estabilidad, veremos que pueden ser muy ventajosos para algunos problemas, mientras que los explícitos serán una opción mejor en otros casos.

Para construir métodos de mayor orden, las estrategias más populares (y que veremos en el curso) incluyen:
- **Métodos multipaso lineales**: en forma similar a lo que hacíamos con diferencias finitas, se propone $y^{n+1}$ como función de múltiples valores $y^j$ y $f^j$, en lugar de usar solo $y^n$ y $f^n/f^{n+1}$ como hicimos para los métodos de Euler.
- **Métodos predictores-correctores**: generan una solución aproximada a partir de métodos de bajo orden (la _predicción_) que luego es mejorada usando algún algoritmo de interpolación (la _corrección_).
- **Métodos de Runge-Kutta**: utilizan, en lugar de valores de $y$ y de $f$ para pasos previos/posteriores, valores intermedios de $f$, como por ejemplo $f^{n+\frac{1}{2}} = f\left(t^n + \frac{k}{2}, y\left(t^n+\frac{k}{2}\right)\right)$. Pueden verse como un caso de métodos predictores-correctores, aunque su uso extendido hace que reciban una denominación propia.

Veamos, antes de pasar a considerar en detalle estas estrategias, por qué nos concentraremos solo en integrar $y$ a partir de su derivada primera $\dot y$. Spoiler: una EDO de orden $N$ puede reducirse a un sistema de $N$ EDOs de primer orden. Vale notar, sin embargo, que existen integradores temporales para EDOs de orden superior, aunque no los veremos en el curso. Cuando en prácticas posteriores veamos ecuaciones en derivadas parciales EDPs, trabajar con integradores para la primer derivada temporal tampoco resultará un limitante, como oportunamente mostraremos.

### **Reducción de dimensionalidad de una EDO (ecuación diferencial ordinaria)**

Si bien en la materia solo veremos integradores temporales para sistemas de EDOs de primer orden, esto no representa restricción alguna con respecto a los problemas que podremos resolver. Como probablemente hayan visto en materias anteriores, cualquier EDO de orden $N$ puede reescribirse como un sistema de $N$ EDOs de primer orden. La manera más simple de verlo es con un ejemplo. Dada el problema de valores iniciales de orden 2
\begin{equation*}
    \dot{y} \ddot{y}\cos(t) + t^2\dot{y}^2 = y \ln(y), \qquad y(t_0) = y_0 \quad \wedge \quad \dot{y}(t_0) = \dot{y}_0,
\end{equation*}
podemos substituir $u_1 = y$, $u_2 = \dot y$, y de esta manera tenemos el sistema
\begin{align*}
    \dot{u}_1 &=  u_2, \qquad \qquad &&u_1(t_0) = y_0, \\
    \dot{u}_2 &= \frac{u_1 \ln(u_1)- t^2 u_2^2}{u_2 \cos(t)}, \qquad \qquad &&u_2(t_0) = \dot{y}_0,
\end{align*}
que es un sistema de 2 ecuaciones diferenciales de primer orden acopladas expresable como
\begin{align*}
    \dot{\mathbf{u}} &= \mathbf{F}(t, \mathbf{u}), \\ \mathbf{u}(t_0) &= \mathbf{u_0},
\end{align*}
con
\begin{equation*}
\mathbf{u} = \begin{pmatrix}
    u_1 \\
    u_2 \\
\end{pmatrix}, \qquad \mathbf{u}_0=\begin{pmatrix}
    y_0\\
    \dot{y}_0
\end{pmatrix}, \qquad \mathbf{F}=\begin{pmatrix}
    u_2 \\
    \dfrac{u_1 \ln(u_1) - t^2 u_2^2}{u_2 \cos(t)}
    \end{pmatrix}.
\end{equation*}

Vemos entonces que, reescribiendo nuestro problema, los métodos que veremos a continuación resultan fácilmente aplicables a ecuaciones de órdenes superiores. Además, para alivianar la notación, presentaremos los esquemas de integración temporal considerando una única EDO de primer orden. Sin embargo, todos los métodos son fácilmente generalizables a sistemas de EDOs, como veremos al final con un ejemplo.

### **Métodos multipaso lineales**

Este tipo de métodos se llaman lineales ya que proponen una combinación lineal (de allí el nombre lineal) de $y^j$ y $f^j$ usando información correspondiente a varios pasos temporales (de allí el nombre multipaso). Esto quiere decir que, en general, podemos escribirlos para un cierto tiempo $t^n$ como
\begin{equation*}
    \sum_{j=0}^s \alpha^j y^{n+j} = k \sum_{j=0}^s \beta^j f^{n+j}, \tag{1}
\end{equation*}
dando valores específicos de $\boldsymbol \alpha$ y $\boldsymbol \beta$ a realizaciones particulares de métodos multipaso lineales. Si bien a simple vista la elección de $\boldsymbol \alpha$ y $\boldsymbol \beta$ puede parecer trivial, i.e. ajustar los $2s+2$ grados de libertad de forma de obtener esquemas de orden $\mathcal{O}(k^{2s})$, si buscamos obtener integradores estables (veremos por qué son los únicos que nos interesan más adelante), el mayor orden de aproximación que podemos obtener con la ecuación $(1)$ es $\mathcal{O}(k^{s+2})$ (Teorema de Dahlquist). De esta manera, aparecen muchas propuestas de cómo ajustar los correspondientes $s$ grados de libertad.

**Una dificultad que presentan los métodos multipaso es que**, por construcción, **resulta necesario conocer $y^j$ y $f^j$ para múltiples instantes**, información con la que no contamos si solo disponemos de una condición inicial $y^0$. **En la práctica, una manera usual de resolver esto es inicializar la integración con otro método** (ej: Euler con alta resolución temporal — de forma de mantener el orden de aproximación—, Runge-Kutta, predictor-corrector, etc.). Una vez contamos con la cantidad de pasos suficientes comenzamos la aplicación del método multipaso deseado.

En la práctica de la materia veremos dos familias de métodos multipaso lineales: los métodos de **Adams-Bashforth** (explícitos) y los de **Adams-Moulton** (implícitos). Ambos métodos en su conjunto se conocen como métodos de _Adams_$^\dagger$ y están dados por la expresión
\begin{equation*}
    y^{n+s} - y^{n+s-1} = k\sum_{j=0}^s \beta^j f^{n+j},
\end{equation*}
es decir, surgen de escoger $\alpha_0, \dots, \alpha_{s-2} = 0$, $\alpha_{s-1} = -1$ y $\alpha_s = 1$.

$^\dagger$: el mismo Adams que postuló por primera vez la existencia de Neptuno.

#### **Adams-Bashforth**

En el método de Adams-Bashforth se busca obtener un método que resulte explícito y por lo tanto se propone $\beta_s = 0$, es decir
\begin{equation*}
    y^{n+s} - y^{n+s-1} = k\sum_{j=0}^{s-1} \beta^j f^{n+j} \tag{Adams-Bashforth}
\end{equation*}
De esta forma solo queda determinar los coeficientes $\beta^{n}, \dots, \beta^{n+s-1}$ y con ello tendremos ya una fórmula que permite determinar $y^{n+s}$ si conocemos $y^{n+s-1}$ y $f^{n}, \dots, f^{n+s-1}$. Para ello, naturalmente, buscaremos un método consistente y maximizar el orden de aproximación a $y^{n+s}$. Para esto consideremos $Y$ una solución exacta a la EDO en cuestión, reemplazando en la última ecuación tenemos
\begin{gather*}
    \left[ Y + \dot{Y}k  + \ddot{Y}\frac{k^2}{2} + \dots \right]_{t^{n+s-1}} - Y^{n+s-1} + \delta Y = k \left\{ \beta^{s-1} f^{n+s-1} + \beta^{s-2} \left[ f - \dot{f} k + \ddot{f} \frac{k^2}{2} + \dots \right]_{t^{n+s-1}} + \\
    + \ldots + \beta^0 \left[  f - \dot{f} (s-1)k + \ddot{f} \frac{(s-1)^2 k^2}{2} + \dots \right]_{t^{n+s-1}} \right\}
\end{gather*}
donde reemplazamos $Y^{n+s}$ por su expansión de Taylor alrededor de $Y^{n+s-1}$, $f^{n+s-j}$ por las correspondientes expansiones alrededor de $f^{n+s-1}$, y $\delta Y$ es el error de truncamiento. Regrupando obtenemos
\begin{equation}
    \delta Y = k \dot Y^{n+s-1} \left[ \beta^{s-1} + \beta^{s-2} + \ldots + \beta^0 - 1\right] + \\
    + k^2 \ddot Y \left[ - \beta^{s-2} - \ldots - (s-1) \beta^0 - \frac{1}{2} \right] + \dots, \tag{Error de truncamiento}
\end{equation}
donde usamos $f^j = \dot Y^j$, $\dot f^j = \ddot Y^j$ y así sucesivamente.

Vemos que para que haya consistencia, debemos pedir $\sum_j \beta^j = 1$, este resultado es general y aplica a todos los métodos multipaso lineales. Los restantes $s$ coeficientes se obtienen intentando cancelar la mayor cantidad de potencias de $k$, de forma de conseguir un método con el mejor orden de aproximación posible para la cantidad de pasos en consideración.

##### **Deducción de coeficientes para un método de 3 pasos**

Para ver un ejemplo de esto, intentemos deducir el método de Adams-Bashforth que utiliza 3 pasos ($s=3$). Es decir, debemos determinar $\beta^0, \beta^1, \beta^2$ ($\beta^3 = 0$ por ser explícito, i.e. un método de Adams-Bashforth). Para ello, escribimos el caso partícular de la ecuación anterior
\begin{gather*}
    \delta Y = k \dot Y \left[ \beta^2 + \beta^1 + \beta^0 - 1\right] + k^2 \ddot Y \left[ -\beta^1 - 2\beta^0 - \frac{1}{2} \right] + \\
    + k^3 \dddot Y \left[ \frac{\beta^1}{2} + \frac{2^2}{2} \beta^0 - \frac{1}{6} \right] + \mathcal{O}(k^4),
\end{gather*}
y pidiendo que se anulen la mayor cantidad de órdenes dominantes posibles (i.e., lo que acompaña a $k$, $k^2$, $k^3$) tenemos
\begin{equation*}
\begin{pmatrix}
     1 &            1 & 1 \\
    -2 &           -1 & 0 \\
     2 &  \frac{1}{2} & 0
\end{pmatrix}
\begin{pmatrix}
    \beta^0\\
    \beta^1\\
    \beta^2
\end{pmatrix}
= \begin{pmatrix}
              1 \\
    \frac{1}{2} \\
    \frac{1}{6}
\end{pmatrix},
\end{equation*}
que tiene como solución
\begin{equation*}
    \beta^0 = \frac{5}{12}, \qquad \beta^1 = - \frac{16}{12}, \qquad \beta^2 = \frac{23}{12},
\end{equation*}
obteniendo un método donde $\delta F = \mathcal{O}(k^4)$, o sea, un método de orden $3$ (recuerden que el orden global es un orden de magnitud menor que el error al cabo de un paso). En general, un método de Adams-Bashforth de $s$ pasos, posee un orden (global) $s$.

En una notación más operativa, podemos escribir el resultado hallado como
\begin{equation}
    y^{n+1} = y^n + \frac{k}{12} \left( 23 f^n - 16 f^{n-1} + 5 f^{n-2} \right).
\end{equation}

Veamos en acción el método que acabamos de derivar. Para eso consideramos el problema de valor inicial
\begin{equation*}
    \dot y (t) = -y(t) + \mathrm{sen}(t), \qquad y(0)=1/2,
\end{equation*}
para $0 \le t \le 10$, que tiene como solución analítica $y(t) = e^{-t} + [\mathrm{sen}(t) - \cos(t)]/2$. Nos valdremos de esta solución analítica para resolver el problema de la inicialización. Esta estrategia no será posible cuando desconozcamos la solución analítica, debiendo recurrir a inicializar la integración con otros métodos, como mencionamos anteriormente.

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

dt    = 2.5e-1              # Paso temporal
y0    = 1/2                 # Condición inicial
tf    = 10                  # Tiempo final de integración
pasos = int(round(tf/dt))   # Cantidad de pasos

y    = np.zeros( pasos+1 )  # Variable para ir guardando la integración

# Agrego los tres primeros pasos
y[0] = y0
y[1] = np.exp(-dt)   + (np.sin(dt)  - np.cos(dt) )/2
y[2] = np.exp(-2*dt) + (np.sin(2*dt)-np.cos(2*dt))/2

# Integro usando AB3
for n in range(2, pasos):
    tn  =     n*dt  # t^n
    tn1 = (n-1)*dt  # t^{n-1}
    tn2 = (n-2)*dt  # t^{n-2}

    fn  = -y[n]   + np.sin(tn)   # f^n
    fn1 = -y[n-1] + np.sin(tn1)  # f^{n-1}
    fn2 = -y[n-2] + np.sin(tn2)  # f^{n-2}

    y[n+1] = y[n] + (23*fn - 16*fn1 + 5*fn2)*dt/12  # Integración explícita

# Grafico
t = np.arange(0, y.size)*dt
fig, ax = plt.subplots(1, 1, figsize=(8,4), constrained_layout=True)
ax.plot(t, y, label=r"AB3 ($k=2,5 \times 10^{-1})$", c="C1", lw=4)
ax.plot(t, np.exp(-t) + (np.sin(t)-np.cos(t))/2, "--k", label="Solución exacta")
ax.legend()
ax.set_title(r"Integración de $\dot{y} = -y + \mathrm{sen(t)}$, $y(0)=1/2$")
ax.set_xlabel("$t$")
ax.set_ylabel("$y$");
```

##### **Resumen de métodos de Adams-Bashforth**

Incluimos a continuación una tabla con los coeficientes que acompañan a cada evaluación de $f$ para los métodos de Adams-Bashforth hasta orden $4$.

| $\mathrm{Pasos}$ | $\mathrm{Orden}$ |      $f^n$     |    $f^{n-1}$   |   $f^{n-2}$    |  $f^{n-3}$    |
|------------------|------------------|----------------|----------------|----------------|---------------|
|        $1$       |        $1$       |     $1$        |      $0$       |       $0$      |       $0$     |
|        $2$       |        $2$       |$\frac{3}{2}$   | $-\frac{1}{2}$ |       $0$      |       $0$     |
|        $3$       |        $3$       |$\frac{23}{12}$ |$-\frac{16}{12}$| $\frac{5}{12}$ |      $0$      |
|        $4$       |        $4$       |$\frac{55}{24}$ |$-\frac{59}{24}$| $\frac{37}{24}$|$-\frac{9}{24}$|

Noten que para $s=1$ recuperamos el método de Euler hacia adelante.

#### **Adams-Moulton**

En los métodos de Adams-Moulton se sigue la misma lógica que la empleada en Adams-Bashforth, pero se relaja la restricción $\beta^s = 0$, resultando por tanto en un método implícito. La forma de obtener los coeficientes apropiados es completamente análoga, obteniendo
la siguiente relación para el error de truncamiento
\begin{equation}
    \delta Y = k \dot Y^{n+s-1} \left[ \beta^s + \beta^{s-1} + \beta^{s-2} + \ldots + \beta^0 - 1\right] + k^2 \ddot Y \left[ \beta^s - \beta^{s-2} - \ldots - (s-1) \beta^0 - \frac{1}{2} \right] + \dots,
\end{equation}
que naturalmente tiene un grado de libertad mayor a la expresión hallada para Adams-Bashforth, dado por $\beta^s$.

##### **Deducción de coeficientes para un método de 3 pasos**

De manera análoga a lo que hicimos para el caso explícito, calculemos los coeficientes para el método implícito (Adams-Moulton) de 3 pasos. Esto implica hallar $\beta^3$, $\beta^2$, $\beta^1$ y $\beta^0$. Para ello escribamos el error de truncamiento hasta un orden apropiado
\begin{gather*}
    \delta Y = k \dot Y \left[ \beta^3 + \beta^2 + \beta^1 + \beta^0 - 1\right] + k^2 \ddot Y \left[ \beta^3 -\beta^1 - 2\beta^0 - \frac{1}{2} \right] + \\
    + k^3 \ddot Y \left[ \frac{\beta^3}{2} + \frac{\beta^1}{2} + \frac{2^2}{2} \beta^0 - \frac{1}{6} \right] + + k^4 \ddot Y \left[ \frac{\beta^3}{6} - \frac{\beta^1}{6} - \frac{2^3}{6} \beta^0 - \frac{1}{24} \right] + \mathcal{O}(k^5).
\end{gather*}
Pedimos nuevamente que se anulen la mayor cantidad de órdenes dominantes posibles, es decir, lo que acompaña a $k$, $k^2$, $k^3$ y $k^4$. Noten que como tenemos un coeficiente más, podemos anular un término más con respecto a lo planteado para Adams-Bashforth. Tenemos entonces
\begin{equation*}
\begin{pmatrix}
               1 &            1 & 1 &          1 \\
              -2 &           -1 & 0 &          1 \\
               2 &  \frac{1}{2} & 0 & \frac{1}{2}\\
    -\frac{4}{3} & -\frac{1}{6} & 0 & \frac{1}{6}
\end{pmatrix}
\begin{pmatrix}
    \beta^0\\
    \beta^1\\
    \beta^2\\
    \beta^3
\end{pmatrix}
= \begin{pmatrix}
               1 \\
     \frac{1}{2} \\
     \frac{1}{6} \\
    \frac{1}{24} \\
\end{pmatrix},
\end{equation*}
que tiene como solución
\begin{equation*}
    \beta^0 = \frac{1}{24}, \qquad \beta^1 = - \frac{5}{24}, \qquad \beta^2 = \frac{19}{24}, \qquad \beta^3 = \frac{9}{24}
\end{equation*}
obteniendo un método $5$. En general, un método de Adams-Moulton de $s$ pasos, posee un orden (global) $s+1$.

En una notación más operativa, podemos escribir el resultado hallado como
\begin{equation}
    y^{n+1} = y^n + \frac{k}{24} \left( 9 f^{n+1} + 19 f^n - 5 f^{n-1} + f^{n-2} \right),
\end{equation}
que es una expresión implícita para $y^{n+1}$.

Al igual que hicimos antes, consideremos el problema de valores iniciales
\begin{equation*}
    \dot y (t) = -y(t) + \mathrm{sen}(t), \qquad \qquad y(0) = 1/2,
\end{equation*}
para $0 \le t \le 10$. Al igual que antes, haremos uso de la solución analítica $y(t) = e^{-t} + \left[\mathrm{sen}(t) - \cos(t) \right]/2$ para inicializar la integración. Además, dado que esta ecuación es lineal, el hecho de que el método sea implícito no supondrá dificultad, ya que puede realizarse el despeje de la siguiente manera
\begin{align*}
    y^{n+1} &= y^n + \frac{k}{24} \left[9(-y^{n+1} + \mathrm{sen}(t^{n+1}) + 19f^n - 5f^{n-1} + f^{n-2} \right]\\
    &= \left[y^n + \frac{k}{24} \left(9\ \mathrm{sen}(t^{n+1}) + 19f^n - 5f^{n-1} + f^{n-2} \right) \right] \frac{1}{1 + \dfrac{9}{24}k}.
\end{align*}

Esto no generalizará a problemas no lineales, donde habrá que usar algún esquema iterativo para hallar $y^{n+1}$.

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

dt    = 2.5e-1              # Paso temporal
y0    = 1/2                 # Condición inicial
tf    = 10                  # Tiempo final de integración
pasos = int(round(tf/dt))   # Cantidad de pasos

y    = np.zeros( pasos+1 )  # Variable para ir guardando la integración

# Condición inicial y primeras iteraciones (las saco de la solución analítica)
y[0] = y0
y[1] = np.exp(-dt)   + (np.sin(dt)  - np.cos(dt) )/2
y[2] = np.exp(-2*dt) + (np.sin(2*dt)-np.cos(2*dt))/2

# Integro usando AM4
for n in range(2, pasos):
    ts  = (n+1)*dt  # t^{n+1}  (tiempo siguiente)
    tn  =     n*dt  # t^n
    tn1 = (n-1)*dt  # t^{n-1}
    tn2 = (n-2)*dt  # t^{n-2}

    fn  = -y[n]   + np.sin(tn)   # f^n
    fn1 = -y[n-1] + np.sin(tn1)  # f^{n-1}
    fn2 = -y[n-2] + np.sin(tn2)  # f^{n-2}

    y[n+1] = (y[n] + (9*np.sin(ts) + 19*fn - 5*fn1 + fn2)*dt/24)/(1+9/24*dt)

# Grafico
t = np.arange(0, y.size)*dt
fig, ax = plt.subplots(1, 1, figsize=(8,4), constrained_layout=True)
ax.plot(t, y, label=r"AM3 ($k=2,5 \times 10^{-1})$", c="C1", lw=4)
ax.plot(t, np.exp(-t) + (np.sin(t)-np.cos(t))/2, "--k", label="Solución exacta")
ax.legend()
ax.set_title(r"Integración de $\dot{y} = -y + \mathrm{sen(t)}$, $y(0)=1/2$")
ax.set_xlabel("$t$")
ax.set_ylabel("$y$");
```

##### **Resumen de métodos de Adams-Moulton**

| $\mathrm{Pasos}$ | $\mathrm{Orden}$ |    $f^{n+1}$    |      $f^n$      |    $f^{n-1}$     |   $f^{n-2}$     |  $f^{n-3}$      |
|------------------|------------------|-----------------|-----------------|------------------|-----------------|-----------------|
|        $1$       |        $1$       |       $1$       |     $0$         |      $0$         |       $0$       |       $0$       |
|        $1$       |        $2$       |$\frac{1}{2}$    |$\frac{1}{2}$    |      $0$         |       $0$       |       $0$       |
|        $2$       |        $3$       |$\frac{5}{12}$    |$\frac{8}{12}$   |$-\frac{1}{12}$   |       $0$       |       $0$       |
|        $3$       |        $4$       |$\frac{9}{24}$   |$\frac{19}{24}$  |$-\frac{5}{24}$   |$\frac{1}{24}$   |      $0$        |
|        $4$       |        $5$       |$\frac{251}{720}$|$\frac{646}{720}$|$-\frac{264}{720}$|$\frac{106}{720}$|$-\frac{19}{720}$|

Vemos que el caso de $1$ paso resulta degenerado, teniendo dos esquemas posibles. El de orden $1$ no es ni más ni ménos que el método de Euler atrasado, mientras que el esquema de segundo orden se conoce como regla trapezoidal.

#### **Otros métodos multipaso lineales**

Sin demostraciones de por medio, podemos mencionar otras familias populares de métodos multipaso lineales. Una opción por ejemplo es buscar operadores temporales centrados, que resulten de la forma
\begin{equation*}
y^{n+s}-y^{n+s-2} = \sum_{j=0}^s \beta^j f^{n+j}.
\end{equation*}
Los métodos explícitos ($\beta^s=0$) asociados a esta elección reciben el nombre de _métodos de Nyström_, mientras que a aquellos implícitos se los conoce como métodos de _Milne-Simpson_, y tienen orden $s$ y $s+1$, respectivamente. Un método de Nyström que vieron en las clases teóricas es aquel conocido como _**Salto de rana**_ (o _leapfrog_ en inglés), dado por
\begin{equation*}
    y^{n+1} = y^{n-1} + 2k f^n,
\end{equation*}
que como vieron en las clases teóricas presenta un modo computacional (es decir, un modo _no-físico_).

Adicionalmente, y de manera opuesta a lo que proponen los métodos de Adams, pueden buscarse métodos con $\beta^0 = \beta^1 = \ldots = \beta^{s-1} = 0$, donde se ajusta $\beta^s$ y $\boldsymbol \alpha$. Estos esquemas reciben el nombre de **_fórmulas de diferenciación hacia atrás_** (o _backward differentiation formulas_ — BDF en inglés—).

### **Métodos predictores-correctores**

Como mencionamos anteriormente, los métodos predictores-correctores consisten en usar dos integradores temporales y combinarlos. La técnica más común es la que combina un método explícito con uno implícito, de la siguiente manera

1. **Paso predictor ($P$)**: Usamos un método explícito para obtener una estimación de $y^{n+1}$, que llamamos $y^{*n+1}$ por ejemplo, utilizando Euler adelantado tendremos:
\begin{equation*}
    y^{*n+1} = y^n + kf^n.
\end{equation*}

2. **Paso evaluador ($E$)**: A partir de la estimación para $y^{n+1}$ obtenida en el paso anterior, calculamos $f(t^{n+1}, y^{*n+1})$, es decir, $f^{*n+1}$.

3. ** Paso corrector ($C$)**: Llamando $f^{*n+1}$ a $f(t^{n+1}, y^{*n+1})$, resolvemos para un esquema implícito. Utilizando ahora como ejemplo Euler atrasado, se obtiene:
\begin{equation}
    y^{n+1} = y^n + kf^{*n+1} = y^n + kf(t^{n+1}, y^n + kf^n). \tag{Matsuno}
\end{equation}
El método que acabamos de hallar es conocido como **_método de Matsuno_** y es $\mathcal{O}(k)$. Análogamente podríamos haber utilizado el esquema trapezoidal (Adams-Moulton de 2do orden) en lugar de Euler atrasado y tendríamos
\begin{equation}
    y^{n+1} = y^n + \frac{k}{2} \left( f^{*n+1} + f^n \right) = y^n + \frac{k}{2}\left[f(t^{n+1}, y^n + kf^n) + f(t^n, y^n) \right], \tag{Heun}
\end{equation}
conocido como **_método de Heun_**, cuyo orden global es $\mathcal{O}(k^2)$.

Noten que en ambos casos acabamos con métodos explícitos, derivados de la combinación de uno explícito y uno implícito.

Vale mencionar que es posible realizar este proceso de manera iterativa, utilizando las ecuaciones de $(\mathrm{Matsuno})$ y $(\mathrm{Heun})$ como nuevos candidatos $y^{*n+1}$, obteniendo un nuevo valor para $f^{*n+1}$ y volviendo a aplicar el método respectivo para obtener $y^{n+1}$. Si realizamos la operación de corrección $c$ veces, suele notarse al método resultante como $P(EC)^c$.

Si bien los métodos predictores-correctores pueden estudiarse más formalmente, no lo haremos en el curso. Solo mencionaremos que otra combinación común para generar métodos predictores-correctores es la conjunción de métodos de Adams-Bashforth (predictor) y Adams-Moulton (corrector), que reciben el nombre de _métodos Adams-Bashforth-Moulton_.

### **Métodos de Runge-Kutta**

A diferencia de los métodos multipaso, que utilizan valores de $y$ y $f$ calculados en pasos anteriores (o para el paso posterior si son implícitos), la idea de los métodos de Runge-Kutta es utilizar múltiples evaluaciones de $f$ entre $t^n$ y $t^{n+1}$ (denominadas _etapas_) para generar una aproximación de mayor orden.

A priori, esto podría parecer ineficiente ya que en los métodos multipaso usamos información ya conocida, mientras que en Runge-Kutta estaremos utilizando nuevas evaluaciones que emplearemos solo para el paso en cuestión. Sin embargo, los métodos de Runge-Kutta precisan menos almacenamiento en memoria. Además presentan distintas características de estabilidad con respecto a los métodos multipaso, por lo que pueden ser más apropiados para algunas EDOs.

Los métodos de Runge-Kutta se clasifican de acuerdo a la cantidad de etapas (así como los multipaso a la cantidad de pasos utilizados). Por ejemplo, para un método de dos etapas tendremos
\begin{equation*}
    y^{n+1} = y^{n} + k \left( \alpha_1 f^n_1 + \alpha_2 f^n_2 \right),
\end{equation*}
donde $f^n_1=f(t^n_1, y^n_1)$ y $f^n_2=f(t^n_2, y^n_2)$ son evaluaciones de $f$ entre $t^n$ y $t^{n+1}$, es decir, $t^n \le t^n_1 \le t^n_2 \le t^{n+1}$. La misma idea aplica para métodos con más etapas. _**Vale resaltar que veremos solo los métodos de Runge-Kutta explícitos ya que son los más empleados**_.

En general, y a diferencia de los métodos multipaso, en el caso de Runge-Kutta no alcanza con definir una cantidad de etapas $s$ y buscar maximizar el orden de aproximación para obtener un método único. Sin embargo, si nos restringimos a métodos cuyas evaluaciones de $f$ están equiespaciadas (aunque en algunos casos repetidas), obtenemos los siguientes esquemas:

* _**Segundo orden**_:

\begin{align}
    y^{n+1} &= y^n + R_2, \tag{Midpoint} \\
    R_1 &= kf\left( t^n, y^n \right), \\
    R_2 &= kf\left( t^n + \frac{k}{2}, y^n + \frac{R_1}{2}\right)
\end{align}

\begin{align}
    y^{n+1} &= y^n + \frac{1}{2} \left( R_1 + R_2 \right), \tag{Heun} \\
    R_1 &= kf\left( t^n, y^n \right), \\
    R_2 &= kf\left( t^n + k, y^n + R_1\right)
\end{align}

* _**Tercer orden**_:
\begin{align}
    y^{n+1} &= y^n + \frac{1}{4}\left( R_1 + 3R_3 \right), \tag{RK3} \\
    R_1 &= kf\left( t^n, y^n \right), \\
    R_2 &= kf\left( t^n + \frac{k}{3}, y^n + \frac{R_1}{3}\right), \\
    R_3 &= kf\left( t^n + 2\frac{k}{3}, y^n + 2\frac{R_2}{3} \right).
\end{align}

* _**Cuarto orden**_:
\begin{align}
    y^{n+1} &= y^n + \frac{1}{6}\left( R_1 + 2R_2 + 2R_3 + R_4\right), \tag{RK4} \\
    R_1 &= kf\left( t^n, y^n \right), \\
    R_2 &= kf\left( t^n + \frac{k}{2}, y^n + \frac{R_1}{2}\right), \\
    R_3 &= kf\left( t^n + \frac{k}{2}, y^n + \frac{R_2}{2} \right), \\
    R_4 &= kf\left( t^n + k, y^n + R_3 \right).
\end{align}

Vemos que tenemos dos métodos de segundo orden. El primero de ellos llamado _**Euler mejorado**_ o _**Punto medio**_ (_midpoint_ en inglés), mientras que el otro es el ya familiar método de Heun. Esto último muestra, como mencionamos anteriormente, que aunque no es del todo usual, es posible considerar a los métodos de Runge-Kutta como esquemas predictores-correctores, solo que operando sobre instantes intermedios (i.e. etapas) en lugar de entre pasos.

Adicionalmente, vemos que nuevamente recuperamos el método de Euler adelantado como el caso degenerado de un método Runge-Kutta de 1 etapa.

Al igual que hicimos antes, para ilustrar el uso de los métodos de Runge-Kutta, integremos el problema de valores iniciales
\begin{equation*}
    \dot y(t) = -y(t) + \mathrm{sen}(t), \qquad \qquad y(0) = 1/2,
\end{equation*}
para $0\le t\le 10$ usando un método de Runge-Kutta de orden 3.

```python
import numpy as np

dt    = 2.5e-1             # Paso temporal
y0    = 1/2                # Condición inicial
tf    = 10                 # Tiempo final de integración
pasos = int(round(tf/dt))  # Cantidad de pasos

# Variable para ir guardando la integración
y    = np.zeros( pasos+1 )
y[0] = y0

# Integro usando RK3
for n in range(0, pasos):
    t1 = n*dt
    R1 = dt*(-y[n] + np.sin(t1))             # Primer etapa

    t2 = t1 + dt/3
    R2 = dt*(-(y[n] + R1/3) + np.sin(t2))    # Segunda etapa

    t3 = t2 + dt/3
    R3 = dt*(-(y[n] + 2*R2/3) + np.sin(t3))  # Última etapa

    y[n+1] = y[n] + (R1 + 3*R3)/4            # Combino las etapas

# Grafico
t = np.arange(0, y.size)*dt
fig, ax = plt.subplots(1, 1, figsize=(8,4), constrained_layout=True)
ax.plot(t, y, label=r"RK3 ($k=2,5 \times 10^{-1})$", c="C1", lw=4)
ax.plot(t, np.exp(-t) + (np.sin(t)-np.cos(t))/2, "--k", label="Solución exacta")
ax.legend()
ax.set_title(r"Integración de $\dot{y} = -y + \mathrm{sen(t)}$, $y(0)=1/2$")
ax.set_xlabel("$t$")
ax.set_ylabel("$y$");
```

### **Aplicación a una EDO de orden superior**

Consideremos ahora el siguiente problema de valores iniciales
\begin{equation*}
    \ddot{y} + \frac{2}{t^2+1} (y - t\dot y) = \bigg( \cos(t) + t\mathrm{sen}(t) \bigg)\frac{2}{t^2+1} - \cos(t), \qquad \qquad y(0) = 2, \qquad \dot{y}(0) = 0.
\end{equation*}
Este problema tiene como solución $y(t) = 1-t^2 + \cos(t)$.

Notemos que llamando $u_0 = y$, $u_1 = \dot{y}$, podemos reducir esta EDO de segundo orden a un sistema de EDOs de primer orden de la siguiente manera
\begin{align*}
    \dot{u}_0 &= u_1, \qquad \qquad & u_0(0) = 2,\\
    \dot{u}_1 &= -\frac{2}{t^2 + 1} (u_0 - tu_1) + \bigg( \cos(t) + t\mathrm{sen}(t) \bigg)\frac{2}{t^2+1} - \cos(t), & u_1(0) = 0.
\end{align*}
Resolvamos este sistema usando un método de Runge-Kutta de orden 3 (RK3).

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

dt    = 1e-2                  # Paso temporal
tf    = 2                     # Tiempo final de integración
pasos = int(round(tf/dt))     # Cantidad de pasos

u = np.zeros( (pasos+1, 2) )  # Arreglo para u_0 (u[:,0]) y u_1 (u[:,1])

# Condiciones iniciales
u[0,0] = 2
u[0,1] = 0

# Variables para guardar los pasos intermedios de Runge-Kutta
R1 = np.zeros( 2 )
R2 = np.zeros( 2 )
R3 = np.zeros( 2 )

# Comienzo a integrar Runge-Kutta de 3er orden (RK3)
for n in range(0, pasos):
    # Primera etapa
    t1 = n*dt
    R1[0] = dt*u[n,1]
    R1[1] = -2*dt/(t1**2+1)*(u[n,0] - t1*u[n,1]) \
                + dt*((np.cos(t1) + t1*np.sin(t1))*2/(t1**2+1) - np.cos(t1) )

    # Segunda etapa
    t2 = t1 + dt/3
    R2[0] = dt*( u[n,1] + R1[1]/3 )
    R2[1] = -2*dt/(t2**2+1)*( u[n,0]+ R1[0]/3 - t2*(u[n,1]+R1[1]/3) ) \
                + dt*((np.cos(t2) + t2*np.sin(t2))*2/(t2**2+1) - np.cos(t2) )

    # Tercera etapa
    t3 = t1 + 2*dt/3
    R3[0] = dt*( u[n,1]+2*R2[1]/3 )
    R3[1] = -2*dt/(t3**2+1)*( u[n,0]+ 2*R2[0]/3 - t3*(u[n,1]+2*R2[1]/3)) \
                + dt*((np.cos(t3) + t3*np.sin(t3))*2/(t3**2+1) - np.cos(t3) )

    # Combino etapas
    u[n+1] = u[n] + (R1 + 3*R3)*1/4

# Grafico
t = np.arange(u.shape[0])*dt
fig, ax = plt.subplots(1, 1, figsize=(8,4), constrained_layout=True)
ax.plot(t, u[:,0], label="Numérica", lw=6, c="C1")
ax.plot(t, (1-t**2) + np.cos(t), "--k", label="Exacta")
ax.legend()
ax.set_title(r"Integración de $\ddot y + \frac{2}{t^2+1}(y-t\dot{y})= " +
             r"(\cos(t)+t\mathrm{sen}(t))\frac{2}{t^2+1}-\cos(t)$"+
             "\n con $y(0) = 2$ y $\ddot{y} = 0$")
ax.set_xlabel("$t$")
ax.set_ylabel("$y$");
```

## **Estabilidad**

### **Regiones de estabilidad**

Una cuestión muy importante que evitamos discutir hasta ahora es la convergencia de la solución numérica a la solución real.

Mencionamos en la práctica anterior que un requisito para que nuestro esquema sea convergente era que el método sea consistente. Esto quiere decir, que en el límite en que la grilla se vuelve infinitamente densa, el operador discreto aplicado a la solución real $Y$ debe converger a la aplicación del operador continuo (lo vimos de esta manera en la práctica anterior, hay otras definiciones equivalentes).

Sin embargo, a la hora de realizar la integración temporal, esto no garantiza que la solución numérica sea convergente. Si inicialmente nuestra solución numérica contiene un pequeño error $\epsilon$, es decir
$y^0 = Y^0 + \epsilon$, ¿qué sucede con $\epsilon$ a medida que integramos en el tiempo?

Para responder esta pregunta es que analizamos la estabilidad. Solo veremos aquí algunas consideraciones generales sobre la estabilidad de distintos esquemas y no todas las maneras de estudiar la estabilidad de un integrador temporal, que es un tópico amplio.

Una manera de analizar la estabilidad es pregntarnos si el esquema temporal propuesto se mantiene acotado al aplicarlo a la ecuación
\begin{equation*}
\dot y = \lambda y,
\end{equation*}
donde $\lambda \in \mathbb{C}$. Noten la similitud con el análisis de Fourier para los errores que vieron en teóricas (estamos mirando el error de amplitud en vez del error de fase). Para $\mathrm{Re}(\lambda) < 0$, un esquema temporal apropiado debería devolver una solución acotada, sin embargo, veremos que esto no siempre es así.

En general, para un dado esquema temporal, habrá valores de $\Delta t$ para los cuales la solución discreta de esta ecuación se mantenga acotada y otros para los cuales no. Estas serán las _**regiones de estabilidad**_ e inestabilidad del método, respectivamente.

### **Ejemplos de regiones de estabilidad**

#### **Euler adelantado**

Como ejemplo podemos estudiar el método de Euler hacia adelante, y el análisis de estabilidad resulta
\begin{equation*}
    y^{n+1} = y^n + k \lambda y^n = (1+k\lambda)y^n.
\end{equation*}
Si ahora lo escribimos en términos de la condición inicial vemos que
\begin{equation*}
    y^n = (1+\bar{\lambda})^n y^0
\end{equation*}
con $\bar \lambda = k \lambda$, que se mantiene acotado solo sí $|1 + \bar{\lambda}| < 1$. Utilizamos la variable $\bar \lambda$ ya que para cualquier esquema acabaremos con un factor $\bar{\lambda} = k \lambda$ al discretizar la ecuación.

Obtuvimos entonces que la _**región de estabilidad**_ para el método de Euler adelantado es la región $|1+k\lambda| < 1$ (i.e. un círculo de radio $1$ en el plano complejo centrado en $z=-1$).

#### **Trapezoidal**

Si consideramos ahora el método trapezoidal tenemos
\begin{equation*}
    y^{n+1} = y^n + \frac{\bar{\lambda}}{2} (y^n + y^{n+1}) \qquad \Longrightarrow \qquad y^{n+1}\left(1 - \frac{\bar \lambda}{2} \right) = y^n \left(1 + \frac{\bar \lambda}{2} \right)
\end{equation*}
y por tanto
\begin{equation*}
    y^n = y_0 \left[ \dfrac{1 + \dfrac{\bar \lambda}{2}}{1 - \dfrac{\bar \lambda}{2}}\right]^n.
\end{equation*}
La región de estabilidad estará dada entonces por $|1+\bar \lambda/2| < |1 - \bar \lambda/2|$, que se satisface siempre que $\mathrm{Re}(\bar \lambda) < 0$, es decir, el método trapezoidal es _**incondicionalmente estable**_ (es estable para todo $k$).

### **Rigidez de una EDO**

La sección previa refirió solo a la estabilidad de EDOs lineales. Sin embargo, para el caso no-lineal, dada una solución a la ecuación a tiempo $t^*$, $y^*$ (que obtuvimos, por ejemplo, numéricamente), podemos linealizar la ecuación alrededor de ($t^*, y^*)$ y hacer un estudio de estabilidad lineal en un entorno de $(t^*$, $y^*)$. Esta estrategia, que no utilizaremos en la materia, funciona aceptablemente en una gran cantidad de casos.

Luego de esta discusión, queda de manifiesto que **la estabilidad al integrar una EDO depende del esquema temporal escogido y de la propia EDO**. De esta observación surge el calificativo de **rígidas** (_stiff_ en inglés) para ecuaciónes diferenciales que requieren pasos temporales muy pequeños para mantener estable su integración. Una formulación alternativa de este concepto, es considerar rígidas a aquellas ecuaciones diferenciales que resultan extremadamente difíciles de integrar con métodos explícitos, que como veremos más adelante, suelen tener regiones de estabilidad más reducidas.

Aún cuando generalmente refiere ecuaciones diferenciales, y así lo usaremos en la materia, el calificativo de rígido es más apropiado para referirse al problema de valores iniciales en su conjunto. Por ejemplo, una EDO puede ser más o menos estable para integrar en función de su condición inicial (i.e. de la solución particular), o del intervalo donde estamos buscando una solución.

Para ver los conceptos de rigidez y estabilidad, consideremos el ejemplo
\begin{equation*}
    \dot y(t) = -100\left[y(t) - \cos(t) \right] - \mathrm{sen}(t), \qquad \qquad y(0) = 1,
\end{equation*}
para $0\le t \le 1$. Este problema de valores iniciales tiene como solución $y(t) = \cos(t)$, y se anula el término entre corchetes.
Veamos que sucede numéricamente

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

dt = 1e-1  # Paso temporal
y0 = 1     # Condición inicial

tf    = 1                  # Tiempo final de integración
pasos = int(round(tf/dt))  # Cantidad de pasos

# Inicializo y para los dos integradores que voy a usar
y_ex1 = np.zeros( pasos+1 )
y_im = np.zeros( pasos+1 )

# Escribo la condición inicial
y_ex1[0] = y0
y_im[0] = y0

# Integro con Euler adelantado (explícito) y atrasado (implícito)
for n in range(0, pasos):
    t_ex =     n*dt  # Tiempo actual
    t_im = (n+1)*dt  # Tiempo siguiente

    # Euler adelantado
    y_ex1[n+1] = y_ex1[n] + dt*(-100*(y_ex1[n]-np.cos(t_ex)) - np.sin(t_ex))

    # Euler atrasado, como la EDO es lineal, puedo despejar y^{n+1}
    y_im[n+1] = (y_im[n] + dt*(100*np.cos(t_im) - np.sin(t_im))) / (1+100*dt)

# Pruebo ahora reducir el paso en un factor 10
dt2      = dt/10
pasos2   = int(round(tf/dt2))
y_ex2    = np.zeros( pasos2+1 )
y_ex2[0] = y0
for n in range(0, pasos2):
    t_ex = n*dt2
    # Euler adelantado
    y_ex2[n+1] = y_ex2[n] + dt2*(-100*(y_ex2[n]-np.cos(t_ex)) - np.sin(t_ex))

# Grafico
t1 = np.arange(0, y_ex1.size)*dt
t2 = np.arange(0, y_ex2.size)*dt2

fig, ax = plt.subplots(1, figsize=(8,4), constrained_layout=True)
ax.plot(t1, y_ex1, label=r"Euler adelantado ($k=1\times10^{-1})$")
ax.plot(t2, y_ex2, label=r"Euler adelantado ($k=1\times10^{-2})$", lw=16,
        alpha=.5)
ax.plot(t1, y_im, label=r"Euler atrasado ($k=1\times10^{-1})$", lw=8, alpha=0.8)
ax.plot(t1, np.cos(t1), '--k', label="Solución exacta", lw=2)
ax.legend(frameon=False, loc="upper left")
ax.set_title("Integración de $\dot y = -100(y-\cos(t)) - \mathrm{sen}(t)$," +
             "  y(0)=1")
ax.set_xlabel("$t$")
ax.set_ylabel("$y$")
ax.set_ylim(0, 2);
```

Vemos que el método explícito diverge para $k=1\times 10^{-1}$, mientras que el implícito se mantiene acotado y además converge a la solución exacta. Esto puede verse a partir de lo que vimos en la sección previa, ya que linealizando la ecuación tenemos $\lambda = -100$ y $\bar \lambda = k \lambda = -10$ está fuera de la región de convergencia del método de Euler adelantado.

Alternativamente, podemos comprender este fenómeno de la siguiente manera. Reparemos en que para una condición inicial ligeramente diferente el término entre corchetes resulta dominante en los instantes iniciales, convergiendo en una escala temporal de $0,01$ ($1/100$) a una solución muy similar a $\cos(t)$. Sin embargo, el método explícito no logra capturar esta escala temporal y acaba por diverger.

Es por ello que un formulación igualmente válida de rigidez, pero más interesante para este curso será la siguiente:

> **Una EDO rígida es aquella que involucra escalas muy diversas.**

Vean que, en nuestro caso, el problema que nos presenta la EDO propuesta es que nos interesa el comportamiento para tiempos $\mathcal{O}(1)$ (el orden del período de $\cos(t)$); sin embargo, para alcanzar una solución aceptable de este comportamiento de baja frecuencia, debemos resolver también las escalas $\mathcal{O}(0,01)$. Pensado físicamente, ese es el motivo por el cual nuestro integrador explícito presenta dificultades, requiriendo 10 veces más pasos para converger a la solución correcta.

Espero que con esto también resulte más clara la importancia de los métodos implícitos que, para el caso no-lineal, pueden requerir un trabajo considerablemente mayor por iteración, con respecto a los explícitos. Sin embargo, generalmente se mantienen estables tomando pasos temporales más grandes. Veremos en el resto de la materia que la elección de un esquema temporal explícito o implícito será problema-dependiente.

### **Resumen de regiones de estabilidad**

Les dejamos a continuación diagramas que esquematizan las regiones de estabilidad de algunas familias de esquemas temporales. En todos los casos los diagramas se hallan en unidades de $\bar \lambda$ (noten las diferencias en los límites de cada panel).

Fuera de esta gráfica quedó el método _Salto de rana_, que solo es estable en la región $\mathrm{Re}(\bar \lambda) = 0 \ \ \wedge \ \ |\mathrm{Im}(\bar \lambda)| \le 1$, siendo apropiado solo para problemas de oscilaciones sin amortiguamiento.

Para los métodos explícitos (Adams-Bashforth y Runge-Kutta) y para Adams-Moulton de orden 3 en adelante, las regiones de estabilidad son las regiones interiores a cada curva (sombreadas en los gráficos). Para Adams-Moulton de orden 1 la región de estabilidad es la exterior a la circunsferencia, mientras que para orden 2 es todo el semiplano $\mathrm{Re}(\bar \lambda) \le 0$. En el caso de diferenciación hacia atrás, las regiones de estabilidad son exteriores a cada una de las curvas.

Recuerden que coinciden los métodos de Runge-Kutta y Adams-Bashforth de orden 1, siendo ambos el método de Euler adelantado. Por su parte, los esquemas de Adams-Moulton de orden 1 y 2 representan a Euler atrasado y a la regla trapezoidal, respectivamente.

Vale notar también que en los métodos multipaso las regiones de estabilidad van disminuyendo a medida que aumenta el orden, mientras que con Runge-Kutta (hasta orden 4) pasa lo contrario.

### **Relación entre convergencia, consistencia y estabilidad**

La importancia de la estabilidad queda resumida en el teorema de Dahlquist$^\dagger$, que enuncia que para un esquema numérico que aproxime a una EDO
\begin{equation}
    \mathrm{Convergente} \qquad \Longleftrightarrow \qquad \mathrm{Consistente} \wedge \mathrm{Estable} \tag{T. de Dahlquist}
\end{equation}

## **Ejemplo integrador: Ecuación de Lane-Emden**

Consideremos ahora el siguiente problema de valores iniciales no lineal
\begin{equation*}
    t^2 \ddot{y} + 2t \dot{y} + t^2 y^\gamma = 0, \qquad \qquad y(0) = 1, \qquad \dot{y}(0) = 0.
\end{equation*}
Esta ecuación es conocida como ecuación de Lane-Emden y describe (adimensionalmente) la presión y la densidad en función de la distancia para una esfera de fluido autogravitante en equilibrio hidrostático, bajo la aproximación politrópica.

Llamando $u_0 = y$, $u_1 = \dot{y}$, podemos reducir esta EDO de segundo orden a un sistema de EDOs de primer orden de la siguiente manera
\begin{align*}
    \dot{u}_0 &= u_1, \qquad \qquad & u_0(0) = 1,\\
    \dot{u}_1 &= - u_0^\gamma - \frac{2u_1}{t} & u_1(0) = 0.
\end{align*}
Noten que para integrar vamos a tener una singularidad al calcular $\dot{u_1}$ en $t=0$. Puede mostrarse que, para todo valor de $\gamma$, las soluciones a la ecuación de Lane-Emden verifican $\ddot y (0) = - y(0)^\gamma/3$. Vamos a aprovechar este resultado para conservar el orden del integrador. Vale remarcar que si no repararamos en este hecho, podríamos integrar la ecuación de cualquier modo, pero los integradores de alto orden temporal verían su orden de precisión disminuido.

Vamos a resolver este sistema utilizando un método de Adams-Moulton de 3er orden (i.e., de 2 pasos). Para ello, tendremos que conseguir de alguna manera $\mathbf{u}^1$ (la solución para el primer paso temporal) y a partir de allí podremos aplicar
\begin{equation*}
    \mathbf u^{n+1} = \mathbf u^n + \frac{k}{12} \left( 5 \mathbf f^{n+1} + 8 \mathbf f^n - \mathbf f^{n-1} \right).
\end{equation*}

Esta ecuación implícita para $\mathbf u^{n+1}$ (aparece en $\mathbf f^{n+1}$ y explícitamente en el miembro izquierdo), la resolveremos mediante un método iterativo de Newton-Krylov (en particular, [LGMRES](https://epubs.siam.org/doi/10.1137/S0895479803422014)) que buscará las raíces de la función
\begin{equation*}
    \mathbf g(\mathbf x) = \mathbf x - \mathbf u^n - \frac{k}{12} \left( 5 \mathbf f(\mathbf x) + 8 \mathbf f^n - \mathbf f^{n-1} \right),
\end{equation*}
donde $\mathbf f(t^{n+1}, \mathbf{x})$ es el sistema de Lane-Emden evaluado en $(t^{n+1}, \mathbf x)$. Vale remarcar que no es necesario que como funcionan los algoritmos de Newton-Krylov, solo precisan saber que permiten obtener raices de forma eficiente y que podemos acceder a él mediante una biblioteca preinstalalada en Colab, SciPy, a través de [`scipy.optimize.newton_krylov`](https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.newton_krylov.html).

Para obtener $\mathbf{u}^1$ vamos a usar otro método de la familia de Adams-Moulton, en particular un método de segundo orden. Para ello utilizaremos la misma idea de búsqueda de raices que mencionamos más arriba pero con el esquema
\begin{equation*}
    \mathbf u^1 = \mathbf u^0 + \frac{k}{2} (\mathbf f^1 + \mathbf f^0).
\end{equation*}

Sin embargo, este método genera $\mathbf u^1$ con un orden de aproximación menor que el que tendrá el integrador que usaremos en adelante (2 y 3 respectivamente). Para mantener el orden de aproximación tendremos que usar a la hora de inicializar un paso temporal $k'$ más chico que el que usaremos luego para continuar la integración, $k$. Este $k'$ deberá verificar ser el mínimo entero mayor que $k^{1-3/2}$ (3 y 2 vienen del orden de integración de cada método), es decir
\begin{equation*}
    k' = \lceil k^{1-\frac{3}{2}} \rceil,
\end{equation*}
con $\lceil \rceil$ la función techo.

El siguiente código define una función que nos permite inicializar la cantidad de pasos deseada para la ecuación de Lane-Emden. También permite ser utilizada para inicializar métodos de otro orden.

```python
def inicializar(cond_inicial, gamma, dt, pasos, orden):
    """ Inicializa Lane-Emden usando Adams-Moulton de 2do orden (trapezoidal)
    utilizando un paso de forma tal de mantener consistente el orden de
    aproximación posterior:
        Entrada:
            - cond_incial: arreglo de dimensiones (2) con las condiciones
                iniciales para u0 y u1.
            - gamma: índice politrópico.
            - dt: paso temporal del método que continuará la integración;
            - pasos: cantidad de pasos a generar usando AM2;
            - orden: orden del método que continuará la integración.
        Devuelve:
            - u: arreglo de dimensiones (pasos, 2) con la ecuación de Lane-Emden
            integrada sobre la cantidad indicada de pasos."""
    import numpy as np
    import scipy.optimize as spoptimize

    # Calculo los pasos para el inicializador por cada paso que debe realizar
    # el integrador de orden superior.
    pasos_ini = dt**(1-orden/2)
    #  pasos_ini seguramente no sea entero. Tomo el primer entero mayor a
    # pasos_ini y defino el dt del inicializador coherentemente
    pasos_ini = int(np.ceil(pasos_ini))
    dt_ini    = dt/pasos_ini

    # Creo un arreglo para ir guardando la integración y agrego condicion inicial
    u_ini    = np.zeros( (pasos_ini*pasos+1, 2) )
    u_ini[0] = cond_inicial

    fn   = np.zeros( 2 )  # Donde voy a ir guardando f^n
    fn1  = np.zeros( 2 )  # Donde voy a ir guardando f^{n-1}

    # Comienzo a integrar usando una regla trapezoidal implícita (AM2)
    for n in range(0, pasos_ini*pasos):
        ts  = (n+1)*dt_ini  # t^{n+1}
        tn  =     n*dt_ini  # t^n

        fn[0]  =  u_ini[n,1]                           # f_1^n
        fn[1]  = -u_ini[n,0]**gamma - 2*u_ini[n,1]/tn  # f_2^n

        # Salvo la singularidad en 0 para y''.
        if n==0: fn[1] = -1/3

        # Obtengo estimación incial para buscar raices de y^{n+1} usando Euler
        est    = u_ini[n] + dt_ini*fn

        # Función a la que le voy a buscar las raices
        def f_raices(us):
            fs = np.array([ us[1], -us[0]**gamma - 2*us[1]/ts ])  # f^{n+1}
            return us - u_ini[n] - (fs + fn)*dt_ini/2             # AM2

        # Le busco las raices usando Newton-Krylov (i.e. obtengo u_ini^{n+1})
        u_ini[n+1] = spoptimize.newton_krylov(f_raices, est)

    # Devuelvo solo los valores de u para el paso temporal original
    return u_ini[pasos_ini::pasos_ini]
```

**Luego de correr esa celda**, ya pueden correr el siguiente código para integrar la ecuación de Lane-Emden con el mecanismo propuesto.

```python
# Vamos a integrar Lane-Emden con Adams-Moulton de 3er orden (2 pasos)
import numpy as np
import scipy.optimize as spoptimize
import matplotlib.pyplot as plt

# Las siguientes dos líneas filtran algunas advertencias que genera el método
# de búsqueda de raices. No las usen en sus códigos.
import warnings
warnings.filterwarnings('ignore')

# Para algunos valores de gamma conozco la solución analítica, la aprovecho.
def sol_analitica(t, gamma):
    if gamma == 0:
        return np.array([ 1-t**2/6, -t/3 ])
    if gamma == 1:
        return np.array([ np.sin(t)/t, (t*np.cos(t) - np.sin(t))/t**2 ])
    if gamma == 5:
        return np.array([ 1/np.sqrt(1+t**2/3), -np.sqrt(3)*t/(t**2+3)**1.5 ])

tf    = 10                             # "Tiempo" final de integración
gamma = 5                              # Índice politrópico
analitica = gamma in [0, 1, 5]         # Veo si existe sol. analitica para gamma
dts   = np.array([ 1e-1, 1e-2, 1e-3 ]) # Pasos temporales a explorar

# Si no tengo solución analitica elijo un único dt para integrar.
if not analitica: dt = np.array([1e-3])

fn   = np.zeros( 2 )  # Donde voy a ir guardando f^n
fn1  = np.zeros( 2 )  # Donde voy a ir guardando f^{n-1}

# Si hay solución analítica voy a ir guardando el error de cada integración
if analitica: errs = np.zeros( (dts.size, 2) )

# Integro para cada dt
for i, dt in enumerate(dts):
    pasos = int(round(tf/dt))     # Cantidad de pasos
    u = np.zeros( (pasos+1, 2) )  # Arreglo para u_0 (u[:,0]) y u_1 (u[:,1])

    # Condiciones iniciales
    u[0,0] = 1
    u[0,1] = 0

    # Inicializo el paso que no puedo hacer con AM3.
    u[1] = inicializar(u[0], gamma, dt, 1, 3)

    # Comienzo a integrar Adams-Moulton de 3er orden (AM3)
    for n in range(1, pasos):
        ts  = (n+1)*dt  # t^{n+1}
        tn  =     n*dt  # t^n
        tn1 = (n-1)*dt  # t^{n-1}

        if n == 1: tn1 = 1e-50  # Para evitar 0/0 al evaluar f en la t=0.

        fn[0]  =  u[n,1]                             # f_1^n
        fn[1]  = -u[n,0]**gamma - 2*u[n,1]/tn        # f_2^n

        fn1[0] =  u[n-1,1]                           # f_1^{n-1}
        fn1[1] = -u[n-1,0]**gamma - 2*u[n-1,1]/tn1   # f_2^{n-1}

        # Salvo la singularidad para y'' en t=0
        if n==1: fn1[1] = -1/3

        # Obtengo estimación incial para buscar raices de y^{n+1} usando Euler
        est    = u[n] + dt*fn

        # Función a la que le voy a buscar las raices
        def f_raices(us):
            fs = np.array([ us[1], -us[0]**gamma - 2*us[1]/ts ])  # f^{n+1}
            return us - u[n] - ( 5*fs + 8*fn - fn1)*dt/12         # AM3

        # Le busco las raices con Newton-Krylov (i.e. obtengo u^{n+1})
        u[n+1] = spoptimize.newton_krylov(f_raices, est)

    # Si tengo solución analítica, calculo el error
    if analitica: errs[i] = np.abs(u[n+1] - sol_analitica(ts, gamma))

# Grafico
if analitica:
    fig, axs = plt.subplots(1, 2, figsize=(8,4), constrained_layout=True)
else:
    fig, ax = plt.subplots(1, 1, figsize=(4,4), constrained_layout=True)
    axs = [ax]

t = np.arange(u.shape[0])*dt
axs[0].plot(t, u[:,0], c="C1", label="Numérica", lw=6)
axs[0].set_xlabel("$t$")
axs[0].set_ylabel("$y$")
fig.suptitle(f"Ecuación de Lane-Emden para $\gamma={gamma}$", fontsize=16)
axs[0].set_title("Solución $y(t)$")
if analitica:
    axs[0].plot(t, sol_analitica(t,gamma)[0], "--k", label="Exacta")
    axs[1].loglog(1/dts, errs[:,0], 'x', label="$y$")
    axs[1].loglog(1/dts, errs[:,1], 'o', label="$\dot{y}$")
    axs[1].loglog(1/dts, 1e-1*dts**3, "--k", label="$\propto k^3$")
    axs[1].legend()
    axs[1].set_title(f"Errores en $y$ e $y'$ para $t={t[-1]:.1f}$")
    axs[1].set_xlabel("$1/k$")
    axs[1].set_ylabel("Error")
axs[0].legend();
```

Como pueden probar ustedes mismos, el código propuesto aprovecha el hecho de que para $\gamma \in \{0, 1, 5 \}$ existe una solución analítica y calcula el error en la integración. Pueden ver que para estos valores de $\gamma$ el orden de precisión del método es el esperado (i.e. $\propto k^3$), excepto para $\gamma = 0$ donde en todos los casos está cerca del error asociado a la precisión aritmética finita.

Vale remarcar finalmente que el uso de un método implícito en este ejemplo conlleva un objetivo puramente pedagógico, de forma de ilustrar su uso para un caso no-lineal y de resolver el problema de la inicialización. Para una ecuación poco rígida como esta resulta más eficiente el uso de algoritmos explícitos, como RK4.

## **Paso adaptativo**

Un elemento que no queríamos dejar de mencionar, aunque no lo veremos en el curso, es que los métodos vistos anteriormente pueden reformularse, con mayor o menor dificultad, para funcionar con $k$ variable. De esta manera, cuando la EDO se comporta más rígidamente se disminuye $k$, mientras que si luego la integración se vuelve menos rígida es posible agrandar el paso. Numerosos solvers automáticos de EDOs, como `scipy.integrate.odeint` implementan esta estrategia.

Sin embargo, esta estrategia no siempre resulta computacionalmente óptima cuando se tiene conocimiento del problema físico en cuestión. Por ejemplo, al integrar las ecuaciones de Navier-Stokes, se conoce a priori el rango de escalas temporales que se deben resolver correctamente y, por tanto, las estrategias con $k$ fijo pueden resultar más apropiadas.

## **Referencias**:
- [_Finite Difference and Spectral Methods for Ordinary and Partial Differential Equations_; N. L. Trefethen (1996)](https://people.maths.ox.ac.uk/trefethen/pdetext.html).
- [_An Introduction to Numerical Modeling of the Atmosphere_; D. A. Randall](http://hogback.atmos.colostate.edu/group/dave/at604pdf/AT604_LaTeX_Book.pdf).