El movimiento de los planetas

prev.gif (997 bytes)chapter.gif (1105 bytes)home.gif (1232 bytes)next.gif (998 bytes)

Ejemplos completos

Introducción

Fundamentos físicos y procedimientos numéricos

Movimiento del planeta

El estado del móvil

La comunicación entre el usuario y el programa

Relación entre los objetos de las distintas clases

Conclusiones

Bibliografía

El código fuente


Introducción

El objetivo del programa interactivo, o applet que se explica en esta página va a consistir en mostrar de forma animada el movimiento de un planeta, pudiendo el usuario detener su movimiento en cualquier instante para tomar datos de su posición y de su velocidad.

 

Fundamentos físicos y procedimientos numéricos.

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 planetario, el cual tiene una solución analítica sencilla en coordenadas polares. La trayectoria seguida por un planeta es una cónica, normalmente una elipse, 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

 

Escalas

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 esa igualdad. Teniendo en cuenta que la aceleración es la derivada segunda de la posición, el movimiento del planeta queda descrito por el siguiente sistema de dos ecuaciones diferenciales de segundo orden

Existen numerosos métodos de resolución de problemas en las que las fuerzas son centrales y conservativas, los más sencillos acumulan en cada paso el error inherente a todo procedimiento numérico, haciendo que el planeta describa una espiral en vez de una elipse que es la trayectoria esperada. El procedimiento elegido denominado de Runge-Kutta es estable y relativamente fácil de programar.

 

Procedimiento numérico de Runge-Kutta

En el Curso de Procedimientos Numéricos en Lenguaje Java, se estudia el procedimiento de Runge-Kutta, para resolver un sistema de dos ecuaciones diferenciales de segundo orden. En este caso, creamos una clase denominada Planeta con una función miembro denominada resolver que nos proporciona la posición de la partícula cada intervalo de tiempo dt, para poder así dibujar la trayectoria del planeta en el contexto gráfico del canvas

public class Planeta {
	private double r; 	//distancia al centro de fuerzas
	private double a, b, c, d;
	private double l1, l2, l3, l4;
	private double k1, k2, k3, k4;
	private double m1, m2, m3, m4;
	private double q1, q2, q3, q4;
	private double dt; 		//intervalo

	public double t;
	public double x;
	public double y;
	public double Vx;
	public double Vy;

Planeta(double x, double y, double Vx, double Vy, double t, double dt){
	this.x=x;
	this.y=y;
	this.Vx=Vx;
	this.Vy=Vy;
	this.dt=dt;
	this.t=t;
}

public void resolver(){
//resolución del sistema de ecuaciones diferenciales de segundo orden
//por el procedimiento de Runge-Kutta
	a=x; b=y; c=Vx; d=Vy;
	l1=c*dt; q1=d*dt;
	r=Math.sqrt(a*a+b*b);
	k1=-a*dt/r/r/r; m1=-b*dt/r/r/r;
	a=x+l1/2; b=y+q1/2; c=Vx+k1/2; d=Vy+m1/2;
	l2=c*dt; q2=d*dt;
	r=Math.sqrt(a*a+b*b);
	k2=-a*dt/r/r/r; m2=-b*dt/r/r/r;
	a=x+l2/2; b=y+q2/2; c=Vx+k2/2; d=Vy+m2/2;
	l3=c*dt; q3=d*dt;
	r=Math.sqrt(a*a+b*b);
	k3=-a*dt/r/r/r; m3=-b*dt/r/r/r;
	a=x+l3; b=y+q3; c=Vx+k3; d=Vy+m3;
	l4=c*dt; q4=d*dt;
	r=Math.sqrt(a*a+b*b);
	k4=-a*dt/r/r/r; m4=-b*dt/r/r/r;
//valores de las variables en el instante t+dt
	t+=dt;
	x+=(l1+2*l2+2*l3+l4)/6;
	y+=(q1+2*q2+2*q3+q4)/6;
	Vx+=(k1+2*k2+2*k3+k4)/6;
	Vy+=(m1+2*m2+2*m3+m4)/6;
  }
}

La clase Planeta tiene una función miembro pública denominada resolver, que calcula la posición del móvil en el instante t+dt cuando se conoce su posición del móvil en el instante t, es decir, resuelve numéricamente el sistema de dos ecuaciones diferenciales de segundo orden.

 

Movimiento del planeta

La clase que describe el applet implementa el interface Runnable y define el método run tal como se aprecia en el cuadro que viene a continuación.

public class PlanetaApplet extends Applet implements Runnable{
//...
  public void run(){
     while(true){
          try{
               hilo.sleep(20);
          }catch(InterruptedException e){}
          if(bMover){
               canvas.mover();
          }
     }
  }  
  void btnEmpieza_actionPerformed(ActionEvent e) {
     double x=Double.valueOf(tPosicion.getText()).doubleValue();
     double Vy=Double.valueOf(tVelocidad.getText()).doubleValue();
     btnPausa.setLabel("  Pausa  ");
     canvas.nuevo(x, Vy);
     if(hilo==null){
          hilo=new Thread(this);
          hilo.start();
     }
     bMover=true;
  }
  void btnPausa_actionPerformed(ActionEvent e) {
     if(bMover==true){
          bMover=false;
          btnPausa.setLabel("Continua");
     }else{
          btnPausa.setLabel("  Pausa  ");
          bMover=true;
     }
  }

Cuando se pulsa el botón titulado "Empieza" se crea un subproceso o thread y se establece el estado inicial (posición inicial y velocidad inicial) de la partícula. Cuando se pulsa el botón "Pausa" cambia su título a "Continua" y cambia de pausa (false) a movimiento (true) y viceversa.

En la función respuesta a la pulsación sobre el botón titulado "Empieza", se crea el subproceso hilo de la clase Thread y se pone en marcha, llamando a start. La función miembro run tiene un bucle que se ejecuta indefinidamente, y que llama a la función mover que mueve al planeta en el área de trabajo del canvas. Además, le hemos añadido la característica de que el planeta detenga su movimiento para que el usuario examine el valor de las variables que describen su estado. El miembro bMover se encarga de esta tarea. Si bMover guarda true se mueve el planeta, en caso contrario se salta la sentencia que llama a la función mover.

 

Representación de la trayectoria

La representación de la trayectoria que sigue el planeta se lleva a cabo en una clase derivada de Canvas denominada MiCanvas, en la que se redefine la función miembro paint. La función paint de la clase base se limita a borrar el área de trabajo del componente, por lo que redefinimos dicha función en la clase derivada para que pinte el origen y los ejes del movimiento.

La función nuevo inicializa los miembros dato del componente cada vez que se comienza una nueva "experiencia", creando un objeto planeta de la clase Planeta y determinando su posición x1 e y1 sobre el área de trabajo del canvas.

La función miembro mover se llama desde la función miembro run, obtiene el contexto gráfico del componente mediante getGraphics, se calcula la nueva posición del planeta, y se dibuja en dicho contexto una línea recta entre las dos posiciones consecutivas del móvil. La posición final x2, y2 será la posición inicial x1 e y1 en la siguiente llamada a la función mover.

La función mover se llama muchísimas veces, por lo que no debemos de olvidarnos de llamar desde el contexto gráfico g a dispose, para liberar los recursos asociados a g.

 void nuevo(double x, double Vy){
     this.x=x;
     this.Vy=Vy;
     t=0.0;
     Vx=0.0;
     y=0.0;
     planeta=new Planeta(x, y, Vx, Vy, t, dt);
     x1=orgX+(int)(x*escala);
     y1=orgY;
     nOrbita++;
     if(nOrbita>13){
          nOrbita=0;
     }
     double mAngular=x*Vy;
     double energia=Vy*Vy/2-2/x;
     parent.estado.setConstantes(energia, mAngular);
  }
  void mover(){
     int x2, y2;
     Graphics g=getGraphics();
//calcula la nueva posición del planeta
     planeta.resolver();
//muestra los valores de las variables
     parent.estado.setEstado(planeta.x, planeta.y, planeta.Vx, planeta.Vy, planeta.t);
     g.setColor(color[nOrbita]);
//situa al planeta en la nueva posición
     x2=orgX+(int)(escala*planeta.x);
     y2=orgY-(int)(escala*planeta.y);

     g.drawLine(x1, y1, x2, y2);
     x1=x2; y1=y2;
     g.dispose();
   }
   public void paint(Graphics g){
     origenEscalas(g);
     dibujaEjes(g);
   }
 }

 

El estado del móvil

Para conocer los valores de la posición, y velocidad del móvil en cada instante, se pueden mostrar en la parte superior del área de trabajo del canvas, convirtiendo los valores numéricos a un string mediante la función miembro valueOf de la clase String, y posteriormente dibujando el texto en el contexto gráfico del componente mediante drawString. Para mostrar el valor de la abscisa x en el punto de coordenadas 20, 30 del contexto gráfico g escribimos.

g.drawString("X: "+String.valueOf(x), 20, 30);

También se puede escribir alternativamente

g.drawString("X: "+x, 20, 30);

ya que se convierte automáticamente el número x, en un string.

No resulta adecuado, mostrar todas las cifras decimales del número x del tipo predefinido double, ya que el procedimiento numérico de cálculo no está libre de errores. Basta en principio, con dos números decimales, para ello podemos emplear dos alternativas una de las cuales emplea la función estática floor de la clase Math.

this.x=(double)((int)(x*100))/100;
this.x=Math.floor(x*100)/100;.

Dado que la posición y velocidad del móvil cambian con el tiempo, sería preciso mostrar sus valores en una zona del contexto gráfico g del canvas, borrar dicha zona y volver a mostrar dichos valores en la nueva posición del móvil. Este proceso da lugar a un parpadeo que es molesto para el usuario. La solución viene dada por la creación de un nuevo componente derivado de la clase Canvas que se ha denominado Estado.

 

El double-buffer

En dicho componente mostramos los valores cambiantes de la posición y velocidad del móvil empleando una técnica denominada double-buffer que se emplea en la mayor parte de las animaciones. El concepto de double-buffer es realmente simple, dibujamos en un contexto en memoria y cuando la imagen esté terminada movemos el bloque entero de datos desde la memoria al contexto gráfico del componente en una única y muy rápida operación.

El primer paso, consiste en redefinir el método update. El método por defecto borra el componente con el color del fondo y llama a la función paint. Podemos cambiar la conducta por defecto, definiendo nuestro propio método update que ya no borra el área de trabajo del componente, sino que llama a la función paint.

public void update(Graphics g){
	paint(g);
} 

Primero, se crea un área en memoria, cuyas dimensiones son las del componente d.width y d.height, respectivamente, y se obtiene su contexto gráfico gBuffer.

imag=createImage(d.width, d.height);
gBuffer=imag.getGraphics();

Se borra dicha área de memoria con el color de fondo del componente

gBuffer.setColor(getBackground());
gBuffer.fillRect(0,0, d.width, d.height);

Se dibujan en dicho contexto en memoria textos, gráficos o imágenes sin afectar para nada el área de trabajo del componente.

muestraValores(gBuffer);

Se transfiere el bloque entero de memoria al contexto gráfico del componente.

public void paint(Graphics g){
	//...
	g.drawImage(imag, 0, 0, null);
}

Finalmente, todo el proceso se realiza mediante una única llamada a la función repaint usualmente desde el método run del thread activo. En nuestro caso indirectamente, una vez que se han actualizado los valores de las variables que definen el estado del móvil.

void setEstado(double x, double y, ...){ 
	this.x=Math.floor(x*100)/100; 
	//... 
	repaint();
}
  void setEstado(double x, double y, double Vx, double Vy, double t){
//dos cifras decimales
     this.x=Math.floor(x*100)/100;
     this.y=Math.floor(y*100)/100;
     this.Vx=Math.floor(Vx*100)/100;
     this.Vy=Math.floor(Vy*100)/100;
     this.t=Math.floor(t*100)/100;
     repaint();
  }
  void muestraValores(Graphics g){
     int cAlto=g.getFontMetrics().getHeight();
     int cAncho=g.getFontMetrics().stringWidth("0");
     g.setColor(Color.black);
     g.drawString("Tiempo", 0, 6*cAlto);
     g.drawString(String.valueOf(t), cAncho, 7*cAlto);
     g.drawString("Posición", 0, 9*cAlto);
     g.drawString("X: "+x, cAncho, 10*cAlto);
//...
   }
  public Dimension getPreferredSize(){
     return new Dimension(80, 300);
  }
  public void update(Graphics g){
     paint(g);
  }
  public void paint(Graphics g){
     Dimension d=getSize();
     if((gBuffer==null)||(d.width!=dim.width)||(d.height!=dim.height)){
          dim=d;
          imag=createImage(d.width, d.height);
          gBuffer=imag.getGraphics();
     }
     gBuffer.setColor(getBackground());
     gBuffer.fillRect(0,0, d.width, d.height);
     muestraValores(gBuffer);
     g.drawImage(imag, 0, 0, null);
 }
}

 

La comunicación entre le usuario y el programa

La comunicación entre el usuario y el programa se realiza mediante la disposición en la superficie del applet de controles y componentes, que permiten introducir los valores iniciales, controlar la evolución del sistema y observar su comportamiento de forma gráfica o animada.

Para introducir los valores iniciales de la posición y velocidad del móvil, se puede hacer con el ratón o bien con controles estándar. En el primer caso, se determina la posición inicial en el lugar en el que se pulsa el botón izquierdo del ratón, y la velocidad se determina por la longitud que se arrastra el ratón manteniendo pulsado el botón izquierdo hasta que se libera.

Mediante controles, se puede optar por controles de edición (TextField) o por barras de desplazamiento (Scrollbars). En el primer caso, sería necesario verificar que la entrada es correcta, que los caracteres son numéricos y que el valor introducido está en el intervalo adecuado. La segunda alternativa es más segura ya que no es necesario verificar nada, solamente es necesario convertir el desplazamiento del dedo de la barra en valores aceptables haciendo un cambio de escala.

Ahora queda la tarea de disponer los controles y los componentes en la superficie del applet.

Figura_B.gif (4580 bytes)

En vez de emplear un gestor de diseño complicado como GridBagLayout, se ha empleado la aproximación de paneles anidados tal como se muestra en la figura.

Se emplea el gestor BorderLayout para disponer el canvas (CENTER) el estado (EAST) y el Panel1 (SOUTH). Sobre el Panel1 situamos el Panel2 (CENTER) y el Panel3 (EAST). Sobre el Panel2 situamos el Panel6 (CENTER) y el Panel7 (SOUTH). Por otra parte, sobre el Panel3 situamos el Panel4 (CENTER) y el Panel5 (SOUTH).

Ahora empleamos el gestor FlowLayout para situar los controles sobre los paneles: en cada uno de los paneles 6 y 7 colocamos una etiqueta (Label) y un control de edición (TextField), alineados a la izquierda.

En el Panel4 colocamos los botones titulados "Empieza" y "Pausa" centrados, y finalmente en el Panel5 colocamos el botón "Borra" con la misma alineación.

Todas estas operaciones se realizan rápidamente con el ratón en el modo de diseño de JBuilder. Los componentes canvas y estado se añaden manualmente en el modo código fuente, y podemos ver el resultado inmediatamente en el modo de diseño, gracias al sistema en el que los cambios en el código se reflejan en el diseño, y cambios en el diseño se reflejan en el código fuente.

Cuando hayamos terminado el diseño, procedemos a definir las funciones respuesta a las acciones del usuario sobre los botones. En el panel de diseño hacemos doble-clic sobre cada uno de los botones. JBuilder genera automáticamente el código acorde con el nuevo modelo denominado Delegation-based event model para la gestión de los sucesos (events) generados por el usuario o por el sistema. El programador solamente tiene que definir la función respuesta cuyo nombre ha generado JBuilder.

Cuando se pulsa el botón titulado "Empieza" se leen los controles de edición y se transforma el texto en valor numérico, que se guarda en las variables locales x y Vx. Dichos valores se pasan al canvas, y se crea el subproceso denominado hilo.

Cuando se actúa sobre el botón titulado "Pausa" se modifica el valor de la variable bMover, y se cambia alternativamente el título del botón.

Cuando las trayectorias no se vean con claridad, se pulsa el botón titulado "Borrar" para llamar a la función paint desde el objeto canvas que borra el área de trabajo de dicho componente y vuelve a dibujar el origen y los ejes.

void btnBorrar_actionPerformed(ActionEvent e) {  
	canvas.repaint();
} 

 

Relación entre los objetos de las distintas clases

Se crean dos objetos canvas y el estado en la clase PlanetaApplet1 para disponerlos sobre la superficie del applet.

public class PlanetaApplet1 extends Applet { 
	MiCanvas canvas;
	Estado estado;

    public void init(){
	canvas=new MiCanvas(this);
	estado=new Estado();
    	this.add(estado, BorderLayout.EAST);
    	this.add(canvas, BorderLayout.CENTER);
	//...
    }
//...
} 

Por otra parte, en la clase MiCanvas se creará un objeto planeta de la clase Planeta, para moverlo por su área de trabajo.

void nuevo(double x, double Vy){
	//...
	planeta=new Planeta(x, y, Vx, Vy, t, dt); 
}

La clase MiCanvas precisa acceder a la función miembro setEstado de la clase Estado para mostrar en su área de trabajo el estado del planeta en movimiento. Esto se puede hacer a través de la referencia que mantiene del applet y que se pasa en el constructor de la clase MiCanvas.

public class MiCanvas extends Canvas { 
	PlanetaApplet parent;

	//...
public MiCanvas(PlanetaApplet p) { 
	parent=p; 

}
void mover(){
	//...
	parent.estado.setEstado(planeta.x, planeta.y, planeta.Vx, planeta.Vy, planeta.t);
}

 

Conclusiones

En este estudio se ha puesto de manifiesto varios aspectos importantes en la programación Java:

  1. La organización del código, creando clases para las distintas tareas:
    La clase Planeta que calcular las posiciones sucesivas del planeta.
    La clase MiCanvas que representa gráficamente la trayectoria.
    La clase Estado que muestra el valor de las variables que describen la dinámica de dicho cuerpo en movimiento.
    La clase PlanetaApplet derivada de Applet que implementa el interface Runnable cuya tarea es la de poner en marcha la aplicación, mover el planeta a intervalos fijos de tiempo,  y posibilitar la comunicación entre el usuario y el programa, por medio de los controles que se disponen en su área de trabajo.
  1. La disposición de los controles y componentes sobre la superficie del applet, empleando el diseño visual de JBuilder.
  1. El uso de subprocesos que posibilitan la programación multitarea.
  1. La representación gráfica de la trayectoria de un móvil, y finalmente el uso de la técnica conocida como double-buffer para evitar el molesto parpadeo que se produce al mostrar y borrar información sucesivamente.

 

Bibliografía

B.P. Demidowitsch, I. A. Maron, E. S. Schuwalowa. Métodos numéricos de análisis. Editorial Paraninfo (1980)

Angel Franco García. Creación de applets educativos: El movimiento de los planetas.  RPP nº 42, Julio-Agosto 1998.

Pedro Agulló Soliveres. Curso de Java (VII) Programación concurrente en Java (I). RPP septiembre de 1997.

David M. Geary. Graphic JAVA 1.1. Mastering the AWT, second edition.  Sun Microsystems (1997).

 

El código fuente

disco.gif (1035 bytes)planeta: PlanetaApplet.java, MiCanvas.java, Estado.java, Planeta.java