Introducción

Los métodos más sencillos como el de Euler o las mejoras de este procedimiento, tienen dos defectos esenciales: la exactitud es pequeña y los errores crecen sistemáticamente. El procedimiento de Runge-Kutta se puede programar fácilmente en los ordenadores, y además se emplea mucho en la práctica, debido a la su exactitud relativamente elevada de la solución aproximada de la ecuación diferencial. La justificación del procedimiento de Runge-Kutta no es sencilla, el lector interesado puede consultar algún libro de métodos numéricos de análisis que como el que se cita en la bibliografía.

Ecuaciones diferenciales de primer orden
Sea una ecuación diferencial de primer orden, con la condición inicial

Se elige una anchura de paso h, y se calculan cuatro números k1, k2, k3, k4 de acuerdo con el procedimiento esquematizado en la tabla adjunta. Según el procedimiento ordinario de Runge-Kutta, a partir del valor de x en el instante t se determina el valor de x en el instante t+h mediante la fórmula que figura en la última fila de dicha tabla.

La jerarquía de clases
Definimos una clase base abstracta denomina RungeKutta, cuya función miembro resolver nos calcule la solución aproximada de la ecuación diferencial x en el instante final tf, cuando le pasamos el estado inicial, es decir, el valor de x0 en el instante inicial t0. La función devuelve el estado del sistema que puede ser la posición del móvil, la intensidad de la corriente eléctrica en un circuito, etc., es decir, el valor de x, en el instante t.
public abstract class RungeKutta { public double resolver(double tf, double t0, double x, double h){ double k1, k2, k3, k4; for(double t=t0; t<tf; t+=h){ k1=h*f(x, t); k2=h*f(x+k1/2, t+h/2); k3=h*f(x+k2/2, t+h/2); k4=h*f(x+k3, t+h); x+=(k1+2*k2+2*k3+k4)/6; } return x; } abstract public double f(double x, double t); }

En la clase Funcion derivada de RungeKutta describimos el modelo físico particular, redefiniendo la función f(x, t). Consideremos la ecuación diferencial que describe la desintegración de una sustancia radioactiva en otra estable.

Donde a es la constante de desintegración radioactiva. A la izquierda tenemos la ecuación diferencial y a la drecha su solución analítica. La definición de la clase Funcion define la función f(x), tomando el valor de la constante de desintegración a igual a 0.1
public class Funcion extends RungeKutta{ public double f(double x, double t){ return (-0.1*x); } }

La llamada a la función resolver se efectuará desde un objeto de la clase derivada Funcion. Se nos pedirá los siguientes datos: el estado inicial, es decir, el número de núcleos x0 en el instante inicial t0, el instante final t en el que queremos calcular el nuevo estado del sistema x, y el paso h para resolver numéricamente la ecuación diferencial. La función miembro resolver devuelve el número de núcleos quedan sin desintegrar en dicho instante t.

double h=0.5; t=0

//paso //número inicial de núcleos en el instante //resolver la e. d. hasta este instante

double x0=5000; double t=20.0;

double x=new Funcion().resolver(t, 0, x0, h); System.out.println("valor aprox. de x "+(int)x);

Comparamos el valor exacto y el valor aproximado, tomando como paso h el valor 0.5, el número inicial de núcleos, 5000, y el instante t, 20. Se obtiene para el resultado aproximado 676 y para el resultado exacto 676, lo que nos confirma la exactitud del procedimiento de Runge-Kutta.
// valor exacto x=(int)(x0*Math.exp(-0.1*t)); System.out.println("valor exacto de x "+(int)x);

Sistema de ecuaciones diferenciales de primer orden
El procedimiento de Runge-Kutta es igualmente efectivo en la resolución de ecuaciones diferenciales de primer orden. Sea el sistema

El procedimiento de aplicación del método de Runge-Kutta a cada una de las ecuaciones diferenciales, con las condiciones iniciales

se esquematiza en la tabla adjunta. Como vemos además de los cuatro números k1, k2, k3, k4 para la primera ecuación diferencial precisamos otros cuatro números l1, l2, l3, l4 para la segunda ecuación diferencial. A partir del valor de x en el instante t, se determina el valor de x en el instante t+h, y a partir del valor de y en el instante t se determina el valor de y en el instante t+h mediante las fórmulas de la última fila de la tabla.

La jerarquía de clases
De modo similar, definimos la clase base abstracta denominada RungeKutta cuya función miembro resolver nos calcule la solución aproximada del sistema de ecuaciones diferenciales de primer orden (x, y) en el instante final tf, cuando le pasamos el estado inicial, es decir, el valor x0 e y0 en el instante inicial t0

public abstract class RungeKutta { public void resolver(double tf, Estado e, double h){ //variables auxiliares double k1, k2, k3, k4; double l1, l2, l3, l4; //estado inicial double x=e.x; double y=e.y; double t0=e.t; for(double t=t0; t<tf; t+=h){ k1=h*f(x, y, t); l1=h*g(x, y, t); k2=h*f(x+k1/2, y+l1/2, t+h/2); l2=h*g(x+k1/2, y+l1/2, t+h/2); k3=h*f(x+k2/2, y+l2/2, t+h/2); l3=h*g(x+k2/2, y+l2/2, t+h/2); k4=h*f(x+k3, y+l3, t+h); l4=h*g(x+k3, y+l3, t+h); x+=(k1+2*k2+2*k3+k4)/6; y+=(l1+2*l2+2*l3+l4)/6; } //estado final e.t=tf; e.x=x; e.y=y; } abstract public double f(double x, double y, double t); abstract public double g(double x, double y, double t); }

Una función solamente, puede devolver un valor, sin embargo, el estado del sistema en cualquier instante t viene dado por dos números x e y. Para ello, definimos la clase denominada Estado con tres miembros públicos x e y y el tiempo t. La función resolver recibe en su segundo parámetro el estado inicial del sistema. Ya que los objetos que se pasan a una función se pueden modificar en el curso de su llamada. La función resolver modifica dicho objeto, devolviendo el estado final cuando se le pasa el estado inicial.
public class Estado { public double t; public double x; public double y; public Estado(double t, double x, double y) { this.t=t; this.x=x; this.y=y; } }

Para obtener el estado e (objeto de la clase Estado) accedemos a sus miembros dato públicos t, x, y

System.out.println("t " +e.t); System.out.println("x " +e.x); System.out.println("y " +e.y);

Para crear el estado e a partir de los valores de t, x, e y, llamamos al constructor de la clase Estado.
double double double Estado t=0; x=1000; y=0; e=new Estado(t, x, y);

Ejemplo
Continuando con el ejemplo de la desintegración radioactiva consideremos ahora, una serie radioactiva de tres elementos en la que, una sustancia radiactiva A se desintegra y se transforma en otra sustancia radiactiva B, que a su vez se desintegra y se transforma en una sustancia C estable. Las ecuaciones diferenciales que gobiernan el proceso y sus soluciones analíticas son, respectivamente,

La solución analítica que aparece a la derecha, se ha obtenido con las condiciones iniciales t=0, x=x0 e y=0. La segunda solución se obtiene siempre que a sea distinto de b. En el caso de que a sea igual a b, la solución analítica para y es

La interpretación del sistema de ecuaciones diferenciales no es complicada. En la unidad de tiempo, desaparecen ax núcleos de la sustancia A al desintegrarse (primera ecuación). En la unidad de tiempo, se producen ax núcleos de la sustancia B, y a su vez desaparecen bx núcleos de la sustancia B que al desintegrarse se transforman en núcleos de la sustancia C estable (segunda ecuación). Para codificar este ejemplo, primero se han de definir las funciones f, g en la clase derivada Funcion de la clase abstracta RungeKutta.

public class Funcion extends RungeKutta{ public double f(double x, double y, double t){ return (-0.1*x); } public double g(double x, double y, double t){ return (0.1*x-0.2*y); } }

Definimos el estado inicial en el instante t0=0, el número de núcleos de la sustancia radiactiva A, suponemos que no hay inicialmente núcleos de sustancia B. Establecemos el instante t en el que deseamos conocer el número de núcleos que hay de la sustancia A, y el número de núcleos de la sustancia B, llamando a la función resolver desde un objeto de la clase Funcion.
double h=0.5; //paso

double x0=5000; Estado es=new Estado(0, x0, 0); double t=20.0; //resolver la e. d. hasta este instante new Funcion().resolver(t, es, h); System.out.println("valor aprox. de x "+(int)es.x); System.out.println("valor aprox. de y "+(int)es.y);

Como en el apartado anterior fijamos el número de núcleos de la sustancia A en 5000, y el de la B ningún núcleo. El valor del paso h lo tomamos como 0.5. Los resultados aproximados para el instante t=20 están en concordancia con los obtenidos a partir del resultado exacto, como queda reflejado en la tabla adjunta t=20 A B Aproximado 676 585 Exacto 676 585

Ecuación diferencial de segundo orden
Vamos a aplicar el procedimiento de Runge-Kutta a una ecuación diferencial de segundo orden.

con las condiciones iniciales

Una ecuación diferencial de segundo orden es equivalente a un sistema de dos ecuaciones diferenciales de primer orden, por lo que aplicaremos el mismo esquema.

Comparando esta tabla con la de un sistema de dos ecuaciones diferenciales de primer orden, vemos que la segunda columna es la misma, excepto por cambio de nombre de la función, f en vez de g, y de la variable, v en vez de y. En la primera columna, las variables k1, k2, k3, k4 pueden calcularse directamente sin efectuar llamadas a una función.

La jerarquía de clases
La definición de la clase abstracta RungeKutta con la función miembro resolver es, la siguiente.
public abstract class RungeKutta { public void resolver(double tf, Estado e, double h){ //variables auxiliares double k1, k2, k3, k4; double l1, l2, l3, l4; //estado inicial double x=e.x; double v=e.v; double t0=e.t; for(double t=t0; t<tf; t+=h){ k1=h*v; l1=h*f(x, v, t); k2=h*(v+l1/2); l2=h*f(x+k1/2, v+l1/2, t+h/2); k3=h*(v+l2/2); l3=h*f(x+k2/2, v+l2/2, t+h/2); k4=h*(v+l3); l4=h*f(x+k3, v+l3, t+h); x+=(k1+2*k2+2*k3+k4)/6; v+=(l1+2*l2+2*l3+l4)/6; } e.t=tf; e.x=x; e.v=v; } abstract public double f(double x, double y, double t); }

La definición de la clase cuyos miembros dato nos sirven para guardar el estado del sistema es similar a la clase definida en el apartado sistema de dos ecuaciones diferenciales de primer orden con el mismo propósito, cambiando el miembro y por la derivada de x, que hemos llamado v.
public class Estado { public double t; public double x; public double v; public Estado(double t, double x, double v) { this.t=t; this.x=x; this.v=v; } }

Ejemplos

Oscilaciones libres

La ecuación diferencial que describe un oscilador armónico y su solución para unas condiciones iniciales fijadas es

donde ω0 es la frecuencia angular, y el periodo del movimiento es P=2π/ω0. La clase denominada Oscilador que describirá un oscilador armónico tendrá como miembro dato, la frecuencia angular, y derivará de la clase base RungeKutta, ya que el comportamiento de un oscilador armónico viene descrito por una ecuación diferencial de segundo orden. La clase Oscilador definirá la función f declarada abstracta en la clase base.
public class Oscilador extends RungeKutta{ protected double w0; //frecuencia angular public Oscilador(double w0){ this.w0=w0; } public double f(double x, double v, double t){ return (-w0*w0*x); } }

Examinemos el comportamiento de un oscilador que en el instante inicial t=0, se encuentra en la posición de máximo desplazamiento x=A, y se suelta con velocidad inicial cero v=0. La amplitud A y la fase inicial ϕ se calculan, a partir de las condiciones iniciales especificadas, que pueden ser distintas de las señaladas. Podemos experimentar con un oscilador de frecuencia angularω0 de 2 rad/s, lo que corresponde a un periodo P=2π/ω de π s, calculemos la posición y la velocidad del oscilador en el instante t= 2s, tomando como paso h el valor de 0.01.
double h=0.01; //paso double w0=2.0; //frecuencia propia oscilaciones libres double t=2.0; //resolver la e. d. hasta este instante //Situación inicial double x0=1.5; //posición inicial double v0=0.0; //velocidad inicial Estado es=new Estado(0.0, x0, v0); //oscilaciones libres System.out.println("Oscilaciones libres"); Oscilador os=new Oscilador(w0); os.resolver(t, es, h); System.out.println("posición aprox. "+es.x);

System.out.println("velocidad aprox. "+es.v); // valor exacto double fi=Math.atan(w0*x0/v0); double A=x0/Math.sin(fi); double x=A*Math.sin(w0*t+fi); double v=A*w0*Math.cos(w0*t+fi); System.out.println("posición exacta "+x); System.out.println("velocidad exacta "+v);

Oscilaciones amortiguadas Si además consideramos que existe un rozamiento que se describe en términos de una fuerza proporcional a la velocidad, la ecuación diferencial del oscilador amortiguado se escribe.

En vez de crear una nueva clase Amortiguado que sustituya a la clase Oscilador, podemos pensar que un oscilador amortiguado, es una clase especial de oscilador, y por tanto la clase que lo describe, derivará de Oscilador, le añadirá el miembro dato g (por γ), que da cuenta de la intensidad de las fuerzas de rozamiento, y además, redefine la función f.
public class Amortiguado extends Oscilador{ protected double g; public Amortiguado(double g, double w0){ super(w0); this.g=g; } public double f(double x, double v, double t){ return (-2*g*v-w0*w0*x); } }

En la figura, se muestra la jerarquía de clases.

Sea por ejemplo, un oscilador armónico amortiguado que tiene los datos ω0=2, γ=0.5, y las condiciones iniciales en el instante son t=0, x=1.5, y v=0. Tomando un paso h de anchura 0.1, se puede comparar la posición del móvil en el instante t, con la deducida a partir de la solución analítica de la ecuación diferencial

La amplitud A y la fase inicial ϕ se calculan a partir de las condiciones iniciales especificadas.
double h=0.01; //paso double w0=2.0; //frecuencia propia oscilaciones libres double g=0.5; //cte de amortiguamiento double t=2.0; //resolver la e. d. hasta este instante //Situación inicial double x0=1.5; //posición inicial double v0=0.0; //velocidad inicial Estado es=new Estado(0.0, x0, v0); //oscilaciones amortiguadas System.out.println("Oscilaciones amortiguadas"); es=new Estado(0.0, x0, v0); new Amortiguado(g, w0).resolver(t, es, h); System.out.println("posición aprox. "+es.x); System.out.println("velocidad aprox. "+es.v); // valor exacto double w=Math.sqrt(w0*w0-g*g); fi=Math.atan(w*x0/(v0+g*x0)); A=x0/Math.sin(fi); x=A*Math.exp(-g*t)*Math.sin(w*t+fi); System.out.println("posición exacta "+x); v=A*Math.exp(-g*t)*(-g*Math.sin(w*t+fi)+w*Math.cos(w*t+fi)); System.out.println("velocidad exacta "+v);

Sistema de dos ecuaciones diferenciales de segundo orden
Sea el sistema de dos ecuaciones diferenciales de segundo orden

con las condiciones iniciales

Este sistema, se puede transformar en un sistema equivalente formado por cuatro ecuaciones diferenciales de primer orden. Aplicando dos veces el esquema descrito para una ecuación diferencial de segundo orden, obtenemos el esquema descrito en las siguientes tablas

La codificación de la función que resuelve el sistema de dos ecuaciones diferenciales de segundo orden, repite dos veces, el código de la función resolver correspondiente a una ecuación diferencial de segundo orden, ya que hay que calcular cuatro pares de parámetros más, los correspondientes a la segunda ecuación diferencial de segundo orden.

La jerarquía de clases
De modo análogo a los apartados anteriores, estableceremos una jerarquía de clases, la clase base abstracta define el procedimiento numérico y la clase derivada define el sistema físico particular.
public abstract class RungeKutta { public void resolver(double tf, Estado e, double h){ //variables auxiliares double k1, k2, k3, k4; double l1, l2, l3, l4; double q1, q2, q3, q4; double m1, m2, m3, m4; //estado inicial double x=e.x; double y=e.y; double vx=e.vx; double vy=e.vy; double t0=e.t; for(double t=t0; t<tf; t+=h){ k1=h*vx; l1=h*f(x, y, vx, vy, t); q1=h*vy; m1=h*g(x, y, vx, vy, t); k2=h*(vx+l1/2); l2=h*f(x+k1/2, y+q1/2, vx+l1/2, vy+m1/2, t+h/2); q2=h*(vy+m1/2);

m2=h*g(x+k1/2, y+q1/2, vx+l1/2, vy+m1/2, t+h/2); k3=h*(vx+l2/2); l3=h*f(x+k2/2, y+q2/2, vx+l2/2, vy+m2/2, t+h/2); q3=h*(vy+m2/2); m3=h*g(x+k2/2, y+q2/2, vx+l2/2, vy+m2/2, t+h/2); k4=h*(vx+l3); l4=h*f(x+k3, y+q3, vx+l3, vy+m3, t+h); q4=h*(vy+m3); m4=h*g(x+k3, y+q3, vx+l3, vy+m3, t+h); x+=(k1+2*k2+2*k3+k4)/6; vx+=(l1+2*l2+2*l3+l4)/6; y+=(q1+2*q2+2*q3+q4)/6; vy+=(m1+2*m2+2*m3+m4)/6;

} //estado final e.x=x; e.y=y; e.vx=vx; e.vy=vy; e.t=tf; } abstract public double f(double x, double y, double vx, double vy, double t); abstract public double g(double x, double y, double vx, double vy, double t); }

El estado del sistema vendrá dado por cuatro números x, y, vx, vy y t, miembros dato de la clase que hemos denominado Estado.
public class Estado { public double t; public double x; public double y; public double vx; public double vy; public Estado(double t, double x, double y, double vx, double vy) { this.t=t; this.x=x; this.y=y; this.vx=vx; this.vy=vy; } }

Ejemplo
Uno de los ejemplos más interesantes de resolución de un sistema de ecuaciones diferenciales de segundo orden es la descripción del movimiento de los planetas, el cual tiene una solución analítica sencilla en coordenadas polares. La trayectoria seguida por un planeta es una cónica, una elipse en particular, en uno de cuyos focos está el centro fijo de fuerzas, el Sol. En la figura, se muestra la fuerza que ejerce el Sol sobre un planeta, inversamente proporcional al cuadrado de las distancias que separan sus centros, y también se muestran sus componentes rectangulares

Teniendo en cuenta que la fuerza que ejerce el sol sobre un planeta viene descrita por la ley de la Gravitación Universal

donde M es la masa del Sol, m la masa del planeta y r la distancia entre el centro del Sol y del planeta. Las componentes de la aceleración del planeta serán

Uno de los problemas del tratamiento numérico con ordenador, es la de reducir el problema a números simples e inteligibles por el usuario de un vistazo. Las masa de los planetas y del Sol son números muy grandes: la masa de la Tierra es 5.98 1024 kg., y 1.98 1030 kg. la del Sol. La distancia media entre la Tierra y el Sol es también muy grande 1.49 1011 m. y la constante G es muy pequeña 6.67 10-11 en el Sistema Internacional de Unidades. Podemos

simplificar el problema numérico, refiriéndonos a un hipotético Sol cuya masa sea tal que el producto GM=1 o bien, que se ha cambiado la escala de los tiempos de modo que se cumpla esta condición. Teniendo en cuenta que la aceleración es la derivada segunda de la posición, el sistema de dos ecuaciones diferenciales de segundo orden quedará

En la clase derivada Planeta redefinimos las funciones f y g.
public class Planeta extends RungeKutta{ public double f(double x, double y, double vx, double vy, double t){ double r=Math.sqrt(x*x+y*y); return (-x/(r*r*r)); } public double g(double x, double y, double vx, double vy, double t){ double r=Math.sqrt(x*x+y*y); return (-y/(r*r*r)); } }

Los pasos para usar la clase Planeta son los siguientes
• • • • •

Establecer la magnitud del paso h Introducir el estado inicial de la partícula (su posición y su velocidad inicial) Establecer el tiempo final t, en el que deseamos calcular el nuevo estado del planeta. Crear un objeto de la clase Planeta y llamar a la función miembro resolver. Mostrar la posición y velocidad final del planeta.
//paso

double h=0.1;

//Situación inicial double x0=1.0; double vy0=1.2; Estado es=new Estado(0.0, x0, 0.0, 0.0, vy0); double t=10.0; //resolver la e. d. hasta este instante new Planeta().resolver(t, es, h); System.out.println("posición aprox. x "+es.x); System.out.println("posición aprox. y "+es.y); System.out.println("velocidad aprox. vx "+es.vx); System.out.println("velocidad aprox. vy "+es.vy);