Hecho por: Iván Sánchez & Samuel Ochoa.
En esta libreta se resuelve una serie de ejercicios de ecuaciones diferenciales ordinarias usando los métodos de Euler y de Runge-Kutta de orden 4. Antes de empezar los ejercicios, se presentan las implementaciones en Python de estos métodos:
import numpy as np
import math as m
import matplotlib.pyplot as plt
def euler(f, x0, y0, xf, n):
"""
Produce la solución aproximada mediante el método
de Euler de la EDO y' = f(x, y) con la condición
inicial y(x0) = y0, dividiendo el intervalo
(x0, xf) en n subintervalos uniformes.
"""
x = np.zeros(n)
y = np.zeros(n)
dx = (xf-x0)/n
x[0] = x0
y[0] = y0
for i in range(n-1):
x[i+1] = x[i] + dx
y[i+1] = y[i] + dx * f(x[i], y[i])
return x, y
def rk4(f, x0, y0, xf, n):
"""
Produce la solución aproximada mediante el método
de Runge-Kutta de orden 4 de la EDO y' = f(x, y)
con la condición inicial y(x0) = y0, dividiendo el
intervalo (x0, xf) en n subintervalos uniformes.
"""
x = np.zeros(n)
y = np.zeros(n)
dx = (xf-x0)/n
x[0] = x0
y[0] = y0
for i in range(n-1):
x[i+1] = x[i] + dx
s1 = f(x[i], y[i])
s2 = f(x[i] + 0.5*dx, y[i] + 0.5*dx*s1)
s3 = f(x[i] + 0.5*dx, y[i] + 0.5*dx*s2)
s4 = f(x[i] + dx, y[i] + dx*s3)
y[i+1] = y[i] + (dx/6.0)*(s1 + 2*s2 + 2*s3 + s4)
return x, y
Se sabe que la solución exacta al problema de valor inicial: $$ \left\{ \begin{align*} y' &= \sin(y) \\ y(a) &= y_a \end{align*} \right. $$
está dada por: $$ y(t) = 2\arctan\left(e^{t-a} \tan\left(\frac{y_a}{2}\right)\right) + 2\pi\left\lfloor\frac{y_a + \pi}{2\pi}\right\rfloor $$
donde el símbolo $\lfloor...\rfloor$ denota la función parte entera (piso). Calcule la solución aproximada por Euler del anterior problema de valor inicial:
En cada caso utilice valores de $\Delta x$ de la forma $0.1\times2^{-k}$ donde $k\in\{0, 1, 2, 3, 4, 5\}$. Realice un gráfico donde compare todas las soluciones numéricas de una misma condición inicial (para cada condición inicial).
Tenemos que resolver $y' = \sin(y)$ con la condición inicial $y(0) = 0$. Primero lo intentaremos de forma analítica.
Reescribimos la ecuación y separamos: $$ y' = \sin(y) \\ \frac{dy}{dt} = \sin(y) \\ \frac{dy}{\sin(y)} = dt $$
Integrando ambos lados de la ecuación: $$ \int\!\frac{1}{\sin(y)}\,dy = \int\!dt \\ \int\!\csc(y)\,dy = t \\ \ln\left(csc(y) - \cot(y)\right) = t $$
No resulta aparente una manera de despejar $y$ en términos de $t$, por lo que se demuestra la necesidad de una solución numérica.
Para la solución exacta nos proveen la función $y(t)$: $$ y(t) = 2\arctan\left(e^{t-a} \tan\left(\frac{y_a}{2}\right)\right) + 2\pi\left\lfloor\frac{y_a + \pi}{2\pi}\right\rfloor $$
Implementada en Python sería:
def y1(t, a, ya):
"""Función y(t) generalizada"""
return (2 * np.arctan(np.exp(t - a) * np.tan(ya / 2.0))
+ 2 * np.pi * np.floor((ya + np.pi) / (2 * np.pi)))
def y1a(t):
"""Función y(t) con la condición y(0) = 0"""
return y1(t, 0, 0)
Tenemos que hallar la solución numérica mediante el método de Euler, por lo que necesitamos definir una función $f(t, y)$ como el lado derecho de $y'$, un intervalo $(x_0, x_f)$ donde se encuentre la solución, y el número divisiones en las que se partirá el intervalo (n).
Vamos a usar el intervalo $(0, 1)$. En lugar del número de divisiones, el enunciado nos pide que utilicemos valores $\Delta x = 0.1 \times 2^{-k}$, donde $k\in\{0,1,2,3,4,5\}$. A partir de $\Delta x$ obtenemos el número de divisiones para cada caso: $$ \Delta x = \frac{x_f - x_0}{n} \Rightarrow n = \frac{x_f - x_0}{\Delta x} $$
A continuación se implementa esta información y se aplica el método de Euler en Python para graficar la solución exacta de la función y sus aproximaciones para cada valor de $\Delta x$:
# Para permitir zoom y desplazamiento.
# Comentar en caso de problemas.
%matplotlib notebook
def graficar1(sol, xmin, xmax, approx, f, x0, y0, xf,
divs, fmts, lbls, ejes=False):
"""Función general para graficar PVI con un metodo
determinado de aproximación numérica.
"""
x = np.linspace(xmin, xmax)
y = sol(x)
fig, ax = plt.subplots()
if ejes:
ax.axhline(0.0, color='k')
ax.axvline(0.0, color='k')
for i, d in enumerate(divs):
xx, yy = approx(f, x0, y0, xf, d)
plt.plot(xx, yy, fmts[i], label=lbls[i])
ax.plot(x, y, 'k', label='Solución exacta.')
ax.set_xlabel('x')
ax.set_ylabel('y')
plt.legend()
plt.show()
def f1(x, y):
"""Lado derecho de y' = sen(y)"""
return np.sin(y)
def nd(x0, xf, dx):
"""Número de divisiones dado el intervalo y dx"""
return int((xf - x0)/dx)
def graficar_1a(xf):
x0 = 0
y0 = 0
dxs = [0.1*2**(-k) for k in range(0, 6)]
divs = [nd(x0, xf, dx) for dx in dxs]
fmts = ['r-*','b-v','g-^','y-*','m-v','c-^']
lbls = [r'$\Delta x$ = ' + str(dx) for dx in dxs]
graficar1(y1a, x0, xf, euler, f1, x0, y0, xf,
divs, fmts, lbls)
graficar_1a(1)
Como se puede observar, la solución exacta de este problema es una función constante, y todas las aproximaciones son exactas también. Esto es debido a que no hay variación de un valor a otro que produzca un error.
Ahora se nos pide resolver el mismo problema pero con la condición inicial $y(0) = 100$. Como ya tenemos la mayor parte del código implementado, simplemente tenemos que definir la función $y(t)$ particular a la nueva condición inicial y graficar:
def y1b(t):
return y1(t, 0, 100)
def graficar_1b(xf):
x0 = 0
y0 = 100
dxs = [0.1*2**(-k) for k in range(0, 6)]
divs = [nd(x0, xf, dx) for dx in dxs]
fmts = ['r-*','b--','g-.','y:','m--','c-.']
lbls = [r'$\Delta x$ = ' + str(dx) for dx in dxs]
graficar1(y1b, x0, xf, euler, f1, x0, y0, xf,
divs, fmts, lbls)
graficar_1b(1)
En este caso la solución si varía a lo largo del intervalo, y se puede apreciar que la aproximación por Euler se vuelve más precisa conforme las divisiones del intervalo son más pequeñas o, lo que es lo mismo, conforme el número de divisiones sea mayor. Además, en todos los casos se nota que el método de Euler pierde precisión conforme se aleja del valor inicial.
Considere los siguientes PVI:
Para cada problema de valor inicial:
Tenemos que resolver el siguiente problema de valor inicial:
$$ \left\{ \begin{align*} y' &= 3x(y+4)^2 \\ y(0) &= 0 \end{align*} \right. $$Primero resolvemos la ecuación diferencial de forma analítica por separación de variables:
$$ \begin{align*} y' &= 3x(y+4)^2 \\ \frac{dy}{dx} &= 3x(y+4)^2 \\ \frac{dy}{(y+4)^2} &= 3x\,dx \\ \int\!\frac{1}{(y+4)^2}\,dy &= \int\!3x\,dx \end{align*} $$Para la integral de la izquierda hacemos un cambio de variable:
$$ u = y + 4 \Rightarrow du = dy \\ \\ \begin{align*} \int\!\frac{1}{u^2}\,du &= \int\!3x\,dx \\ \int\!u^{-2}\,du &= \int\!3x\,dx \end{align*} $$Integrando a ambos lados tenemos la solución general:
$$ \begin{align*} \frac{u^{-1}}{-1} &= 3\cdot\frac{x^2}{2} + C \\ -\frac{1}{u} &= \frac{3}{2}x^2 + C \\ -\frac{1}{y + 4} &= \frac{3}{2}x^2 + C \\ \end{align*} $$Calculamos la constante de integración dada la condición inicial $y(0) = 0$:
$$ -\frac{1}{0 + 4} = \frac{3}{2}(0)^2 + C \\ \Rightarrow C = -\frac{1}{4} $$Luego reemplazamos esta constante en la solución general y despejamos la $y$ en términos de $x$ para tener la solución particular de la ecuación diferencial:
$$ \begin{align*} -\frac{1}{y + 4} &= \frac{3}{2}x^2 - \frac{1}{4} \\ \frac{1}{y + 4} &= \frac{1}{4} - \frac{3}{2}x^2 \\ \frac{1}{y + 4} &= \frac{1 - 6x^2}{4} \\ 4 &= (y+4)(1 - 6x^2) \\ \frac{4}{1 - 6x^2} &= y + 4 \\ y &= \frac{4}{1 - 6x^2} - 4 \\ \end{align*} $$Antes de continuar, como la función es racional, hay que hallar las asíntotas en el eje horizontal para evitarlas. Estas corresponden a los valores para los cuales el denominador es nulo:
$$ 1 - 6x^2 = 0 \Rightarrow x = \pm\sqrt{\frac{1}{6}} \Rightarrow x \approx \pm0,408248290463863 $$Una vez obtenida esta solución particular exacta del problema, procedemos a obtener la solución numérica por el método de Euler y su error porcentual en cada punto. Nos piden que utilicemos un valor de $\Delta x = 0.1$, por lo que hay que ajustar el número de divisiones (n) de acuerdo a esto. A continuación se presenta la implementación en Python de estos procesos:
import pandas as pd
def f2a(x, y):
"""Lado derecho de y' = f(x, y)"""
return 3*x * (y + 4)**2
def sol2a(x):
"""Solución particular del problema"""
return 4/(1 - 6*x**2) - 4
def error_porcentual(x, y, f):
"""Error porcentual del valor aproximado con respecto
al valor exacto de la función dado por f(x).
"""
fx = f(x)
if fx == 0: return 0.0
return 100.0 * np.absolute((y - f(x)) / f(x))
def generar_tabla(f, x, y, error):
"""Genera una tabla Pandas con los valores aproximados
(x, y), el valor exacto f(x) y el error relativo a este.
"""
return pd.DataFrame({
'x': x,
'y': y,
'y(x)': [f(xi) for xi in x],
'Error relativo (%)': error
})
def resolver_2a(metodo):
"""Resuelve numéricamente el ejercicio 2.a usando
un método determinado.
"""
x0 = 0.0
y0 = 0.0
xf = 0.4 # Para evitar la asíntota en x = 0.408248...
n = nd(x0, xf, 0.1) # Número de divisiones según dx
x, y = metodo(f2a, x0, y0, xf, n)
error = [round(error_porcentual(xi, yi, sol2a), 2)
for xi, yi in zip(x, y)]
return x, y, error
resultados_2a_euler = resolver_2a(euler)
tabla_2a_euler = generar_tabla(sol2a, *resultados_2a_euler)
tabla_2a_euler
Luego, aplicamos el mismo procedimiento pero con el método de Runge-Kuta de orden 4:
resultados_2a_rk4 = resolver_2a(rk4)
tabla_2a_rk4 = generar_tabla(sol2a, *resultados_2a_rk4)
tabla_2a_rk4
Comparando los valores de las tablas, se nota que el método de Runge-Kuta de orden 4 es mucho más preciso que el método de Euler. A continuación se grafica la solución exacta y ambas aproximaciones numéricas para apreciar mejor la discrepancia:
def graficar2(sol, xmin, xmax, f, x0, y0, xf, n, fmts, ejes=False):
"""Función general para graficar PVI con ambos
métodos de aproximación numérica (Euler y RK4).
"""
x = np.linspace(xmin, xmax)
y = sol(x)
fig, ax = plt.subplots()
if ejes:
ax.axhline(0.0, color='k')
ax.axvline(0.0, color='k')
xe, ye = euler(f, x0, y0, xf, n)
plt.plot(xe, ye, fmts[0], label='Método de Euler')
xr, yr = rk4(f, x0, y0, xf, n)
plt.plot(xr, yr, fmts[1], label='Método de RK4')
ax.plot(x, y, 'k', label='Solución exacta.')
ax.set_xlabel('x')
ax.set_ylabel('y')
plt.legend()
plt.show()
def graficar2a():
x0 = 0
y0 = 0
xf = 0.4
n = nd(x0, xf, 0.1)
graficar2(sol2a, x0, xf - 0.05, f2a, x0, y0, xf, n,
['r+','b^'])
graficar2a()
Tenemos que resolver el siguiente problema de valor inicial:
$$ \left\{ \begin{align*} y' &= e^{x+y}\\ y(0) &= 0 \end{align*} \right. $$Primero resolvemos la ecuación diferencial de forma analítica por separación de variables:
$$ \begin{align*} y' &= e^{x+y} \\ \frac{dy}{dx} &= e^{x+y} \\ \frac{dy}{dx} &= e^{x}\,e^{y} \\ \frac{dy}{e^{y}} &= e^{x}\,dx \\ \int\!{e^{-y}}\,dy &= \int\!e^{x}\,dx \end{align*} $$Integrando a ambos lados tenemos la solución general:
$$ \begin{align*} \frac{e^{-y}}{-1} &= {e^{x}} + C \\ -e^{-y} &= {e^{x}} + C \\ \end{align*} $$Calculamos la constante de integración dada la condición inicial $y(0) = 0$:
$$ -e^{-0} = e^{0} + C \\ -1 = 1 + C \\ \Rightarrow C = -2 $$Luego reemplazamos esta constante en la solución general y despejamos la $y$ en términos de $x$ para tener la solución particular de la ecuación diferencial:
$$ \begin{align*} -e^{-y} &= {e^{x}} - 2 \\ e^{-y} &= -{e^{x}} + 2 \\ ln(e^{-y}) &= ln(2 -{e^{x}})\\ {-y} &= ln(2 -{e^{x}}) \\ {y} &= -ln(2 -{e^{x}}) \\ \end{align*} $$Antes de continuar, hay que tomar en cuenta que la función logaritmo neperiano solo está definida para valores en los que su argumento es mayor a cero:
$$ 2 -{e^{x}} > 0 \Rightarrow {e^{x}} < 2 \Rightarrow x < ln(2) \Rightarrow x < 0,6931471805599453 \text{ (aprox)} $$Entonces, la función solución está definida para valores menores a $ln(2)$, por lo que el intervalo no puede pasar dicho valor. Con esta información, procedemos a obtener la solución numérica por el método de Euler y su error porcentual en cada punto. Nos piden que utilicemos un valor de $\Delta x = 0.1$, por lo que hay que ajustar el número de divisiones (n) de acuerdo a esto. A continuación se presenta la implementación en Python de estos procesos:
def f2b(x, y):
"""Lado derecho de y' = f(x, y)"""
return np.exp(x + y)
def sol2b(x):
"""Solución particular del problema"""
return -np.log(2 - np.e**x)
def resolver_2b(metodo):
"""Resuelve numéricamente el ejercicio 2.a usando
un método determinado.
"""
x0 = 0.0
y0 = 0.0
xf = 0.6 # x tiene que ser menor a 0.693147...
n = nd(x0, xf, 0.1) # Número de divisiones según dx
x, y = metodo(f2b, x0, y0, xf, n)
error = [round(error_porcentual(xi, yi, sol2b), 2)
for xi, yi in zip(x, y)]
return x, y, error
resultados_2b_euler = resolver_2b(euler)
tabla_2b_euler = generar_tabla(sol2b, *resultados_2b_euler)
tabla_2b_euler
Luego, aplicamos el mismo procedimiento pero con el método de Runge-Kuta de orden 4:
resultados_2b_rk4 = resolver_2b(rk4)
tabla_2b_rk4 = generar_tabla(sol2b, *resultados_2b_rk4)
tabla_2b_rk4
Comparando los valores de las tablas, se nota que el método de Runge-Kuta de orden 4 es mucho más preciso que el método de Euler. Además, el método de Euler se vuelve cada vez menos preciso conforme se aleja del punto inicial. A continuación se grafica la solución exacta y ambas aproximaciones numéricas para apreciar mejor la discrepancia:
def graficar2b():
x0 = 0
y0 = 0
xf = 0.6
n = nd(x0, xf, 0.1)
graficar2(sol2b, x0, xf, f2b, x0, y0, xf, n,
['r+','b^'])
graficar2b()