/* Lagrange.java: a Java applet for animating finite dimensional * dynamical systems. Based on Lagrangian mechanics. * * Author: Peter Selinger. * * You are free to copy, distribute, and modify this software under * the terms of the GNU General Public License, as long as you give * appropriate credit to the original author and subsequent authors. * See www.gnu.org. */ /* desired changes/additions: * - clean up trace state, redraw state, and totalenergy handling in MechSystem * - identify classes by their names - classloader? * - add display for potential/kinetic energy */ /* >> 23 March, 2001. * >> * >> Modifications by Peter Lynch, Met Eireann, Glasnevin Hill, Dublin 9. * >> * >> Addition of Swinging Spring (three dimensional elastic pendulum). * >> * >> The original forward-backward method with energy adjustment (step) * >> gave results for the resonant swinging spring (three-dimensional * >> elastic pendulum) which were seriously in error. So, two new ode * >> solvers were added for greater accuracy: * >> * >> heun: energy grows slowly during integration. * >> rkstep: Fourth-order Runge-Kutta; good conservation of energy. * >> * >> By default, rkstep is called for the case of the swinging spring: * >> if (type.equals("swingingspring")) { mechsys.rkstep(dt); } * >> * >> Plotting: * >> * >> Both Vertical (x-z) and horizontal (x-y) projections are plotted. * >> Size of the bob indicates distance from the viewer. * >> */ import java.applet.*; import java.awt.*; import java.lang.Math; import java.awt.event.*; import java.util.Vector; // ----------------------------------------------------------------- /** main applet class: provides top-level user interface. Reads applet parameters, supplies applet info, provides methods to start and stop animation etc. */ public class Lagrange extends Applet implements Runnable { static final String version = "0.13"; // parameters String type; // the type of physical system double param[]; // to hold parameters that specify the physical system double q[], qp[]; // to hold parameters about the initial state int frames; // frames per second double timefactor; // fast or slow motion boolean trace; // are we painting a trace? boolean spoor; // are we painting a spoor? boolean controls; // should we display a control panel? Thread running = null; // the thread object for this applet. MechSystem mechsys; SystemCanvas systemcanvas; ControlPanel controlpanel; /** applet info support */ public String getAppletInfo() { return "Name: Lagrange\n" + "Author: Peter Selinger\n" + "Version: "+version+"\n" + "Documentation: http://www.math.lsa.umich.edu/~selinger/lagrange/\n"; } /** applet parameter information */ public String[][] getParameterInfo() { String[][] generalinfo = { { "type", "String", "which type of physical system" }, { "frames", "int", "frames per second" }, { "timefactor", "double", "slow motion < 1.0 < fast motion" }, { "trace", "boolean", "show trace?" }, { "spoor", "boolean", "show spoor?" }, { "controls", "boolean", "show controls?" }, { "bgcolor", "#xxxxxx", "background color" }, { "fgcolor", "#xxxxxx", "foreground color" }, { "tracecolor", "#xxxxxx", "color of the trace" }, { "spoorcolor", "#xxxxxx", "color of the spoor" }, { "scalecolor", "#xxxxxx", "color of the metric scale" }, { "timecolor", "#xxxxxx", "color of the time scale" }, }; Vector infov = new Vector(); for (int i=0; i 3) dt = 3*idealdt; // advance the simulation by dt and repaint if (type.equals("swingingspring")) { // mechsys.heun(dt); mechsys.rkstep(dt); } else { mechsys.step(dt); } systemcanvas.repaint(1L); } catch (InterruptedException e) { stop(); } } } } /** a panel for five buttons optionally displayed in the main applet window */ class ControlPanel extends Panel implements WindowListener { MechSystem mechsys; SystemCanvas systemcanvas; Lagrange lagr; Button restartbutton, startstopbutton, tracebutton, spoorbutton, changebutton; ControlPanel(MechSystem mechsys, SystemCanvas systemcanvas, Lagrange lagr) { this.mechsys = mechsys; this.systemcanvas = systemcanvas; this.lagr = lagr; // setBackground(Global.bgcolor); setBackground(Color.lightGray); // create components restartbutton = new Button("Restart"); startstopbutton = new Button("Pause/Resume"); tracebutton = new Button("Trace On/Off"); spoorbutton = new Button("Spoor On/Off"); changebutton = new Button("Change Parameters"); add(restartbutton); add(startstopbutton); add(tracebutton); add(spoorbutton); add(changebutton); } /** handles events from the buttons */ public boolean action(Event e, Object arg) { Object target = e.target; if (target == restartbutton) { return systemcanvas.restartaction(); } if (target == startstopbutton) { return lagr.startstopaction(); } if (target == tracebutton) { return systemcanvas.traceaction(); } if (target == spoorbutton) { return systemcanvas.spooraction(); } if (target == changebutton) { if (Global.controlframe == null) { // if there is no control frame yet, create one Global.controlframe = new ControlFrame(mechsys, systemcanvas, this); // add self as a listener for window events Global.controlframe.addWindowListener(this); } Global.controlframe.show(); return true; } return false; } void closecontrolframe() { Global.controlframe.dispose(); Global.controlframe = null; } //---------------------------------------------------------------------- // WindowListener interface public void windowClosing(WindowEvent e) { if (e.getWindow()==Global.controlframe) { closecontrolframe(); } } public void windowOpened(WindowEvent e) {} public void windowClosed(WindowEvent e) {} public void windowIconified(WindowEvent e) {} public void windowDeiconified(WindowEvent e) {} public void windowActivated(WindowEvent e) {} public void windowDeactivated(WindowEvent e) {} } /** provides a window in which the applet parameters can be edited. */ class ControlFrame extends Frame { MechSystem mechsys; SystemCanvas systemcanvas; ControlPanel controlpanel; int k; // the number of parameters in our mechsys. TextField[] paramentry; TextField energyentry; TextField framesentry; TextField timefactorentry; Button dismissbutton, applybutton; ControlFrame(MechSystem mechsys, SystemCanvas systemcanvas, ControlPanel controlpanel) { super("Lagrange Controls"); this.mechsys = mechsys; this.systemcanvas = systemcanvas; this.controlpanel = controlpanel; setTitle("Lagrange Controls"); Parameter[] paraminfo = mechsys.getParamInfo(); k = paraminfo.length; // prepare layout setLayout(new GridLayout(0,2)); // create components add(new Label("Lagrange Applet Controls")); add(new Label(mechsys.getType())); paramentry = new TextField[k]; for (int i=0; i10 || e<-10) { e=((int)(10*e))/10.0; } else { e=((int)(1000*e))/1000.0; } energyentry.setText(String.valueOf(e)); } void updateentry(int i) { try { mechsys.param[i]= Double.valueOf(paramentry[i].getText()).doubleValue(); } catch (NumberFormatException exc) {} } void updateframes() { try { mechsys.frames=Integer.valueOf(framesentry.getText()).intValue(); } catch (NumberFormatException exc) {} } void updatetimefactor() { try { mechsys.timefactor= Double.valueOf(timefactorentry.getText()).doubleValue(); } catch (NumberFormatException exc) {} } void updateenergy() { try { mechsys.setEnergy(Double.valueOf(energyentry.getText()).doubleValue()); mechsys.scalerequest = true; } catch (NumberFormatException exc) {} } void updateentries() { for (int i=0; i (2*d.height) ) { // draw mullion in wide window bg.setColor(Color.lightGray); bg.fillRect((d.width)/2, bw, bw, d.height-2*bw); } // initialize background image mechsys.initBackground(bg,d); } // repaint off-screen image with trace image if (bgImage != null) { og.drawImage(bgImage, 0, 0, null); } mechsys.drawsystem(og,bg); // repaint existing image with off-screen image if (offImage != null) { g.drawImage(offImage, 0, 0, null); } } public void paint(Graphics g) { update(g); } // ---------------------------------------------------------------------- // Event handling public boolean mouseDown(Event evt, int x, int y) { return mechsys.mouseDown(evt,x,y,this); } public boolean mouseDrag(Event evt, int x, int y) { return mechsys.mouseDrag(evt,x,y,this); } public boolean mouseUp(Event evt, int x, int y) { return mechsys.mouseUp(evt,x,y,this); } // ------------------- // handle buttons from controlpanel public boolean restartaction() { mechsys.resetstate(); repaint(); return true; } public boolean traceaction() { mechsys.toggletrace(); repaint(); return true; } public boolean spooraction() { mechsys.togglespoor(); repaint(); return true; } } /** parent class for all mechanical systems. Provides common code to implement the numerical solution of the Lagrangian equations of motion, and for drawing and handling mouse events. Subclasses must supply information that is specific to dynamical systems, such as parameters and coordinates, kinetic and potential energy, and specific drawing routines. */ abstract class MechSystem { final static double epsilon=0.0001; boolean button=false; // is the mouse button pressed? boolean preserveenergy = true; boolean stopped=true; double param[]; // holds the physical parameters. int frames; // holds the frames per second double timefactor; // > 1.0 for fast motion, < 1.0 for slow motion // for tracing int tracemode; final static int NOTRACE=0, NEWTRACE=1, TRACING=2; int oldx, oldy; // for spooring int spoormode; final static int NOSPOOR=0, NEWSPOOR=1, SPOORING=2; int oldxx, oldyy; CoordinateSystem c; boolean scalerequest = true; // dynamic variables to hold the state double q[]; // coordinates q1, q2, q3... double qp[]; // their derivatives q1', q2', ... boolean fixed[]; // is the ith variable currently fixed? double oldq[], oldqp[]; // remember initial state double totalenergy; // workspace for stepping function double A[][]; double rhs[]; double K0[], K1[], K2[], K3[]; // Workspace for Heun and Runge-Kutta double qpp[]; // the second derivatives q1'', q2'', ... double qn[]; // requested value for constrained coordinates // subclasses must override these methods that define the physical // characteristics of the system. abstract String getType(); abstract Parameter[] getParamInfo(); abstract Coordinate[] getCoordInfo(); int N; // number of coordinates Coordinate[] coordinfo; // info about coordinates; notably their periods // if a subclass provides an explicit initializer, it must call this MechSystem() { coordinfo = getCoordInfo(); N = coordinfo.length; q = new double[N]; // create vectors to hold state qp = new double[N]; oldq = new double[N]; // create vectors to hold state oldqp = new double[N]; A = new double[N][N]; // and workspace rhs = new double[N]; qpp = new double[N]; // second derivative of q qn = new double[N]; // requested value for fixed coordinate fixed = new boolean[N]; K0 = new double[2*N]; // Intermediate vectors for Heun K1 = new double[2*N]; K2 = new double[2*N]; // and for Runge-Kutta solvers. K3 = new double[2*N]; } void appendInfoVector(Vector infov) { Parameter[] paraminfo = getParamInfo(); String type = getType(); for (int i=0; i 0) drawTimeFactor(bg,10,10,s); scalerequest = false; } String getTimeFactorString(double timefactor) { String s = ""; boolean reverse = false; if (timefactor < 0.0) { s = "Reverse "; reverse = true; timefactor *= -1; } if (timefactor == 0.0) { s += "Stopped Motion"; } else if (timefactor > 1.0) { s += "Fast Motion "+stringFromDouble(timefactor)+" : 1"; } else if (timefactor < 1.0) { s += "Slow Motion "+"1 : "+stringFromDouble(1/timefactor); } else { if (reverse) s += "Real Time"; } return s; } /** convert a double that is greater than 1.0 to a reasonable string */ String stringFromDouble(double d) { int n = (int)(d*10+.5); if (n%10==0) return String.valueOf(n/10); else return String.valueOf(((double)(n))/10); } void drawTimeFactor(Graphics g, int x, int y, String s) { g.setColor(Global.timecolor); FontMetrics fontmetrics = g.getFontMetrics(); int fontheight = fontmetrics.getHeight(); g.drawString(s, x, y+2*fontheight+1); } void recordTrace(Graphics bg, Graphics og, double xx, double yy) { int x = c.x(xx); int y = c.y(yy); switch (tracemode) { case TRACING: bg.setColor(Global.tracecolor); bg.drawLine(oldx,oldy,x,y); og.setColor(Global.tracecolor); og.drawLine(oldx,oldy,x,y); oldx=x; oldy=y; break; case NEWTRACE: oldx=x; oldy=y; tracemode=TRACING; break; default: break; } } void recordSpoor(Graphics bg, Graphics og, double xx, double yy) { int x = c.x(xx); int y = c.y(yy); switch (spoormode) { case SPOORING: bg.setColor(Global.spoorcolor); bg.drawLine(oldxx,oldyy,x,y); og.setColor(Global.spoorcolor); og.drawLine(oldxx,oldyy,x,y); oldxx=x; oldyy=y; break; case NEWSPOOR: oldxx=x; oldyy=y; spoormode=SPOORING; break; default: break; } } /* subclasses must override this: draw yourself in og/bg */ abstract void drawsystem(Graphics og, Graphics bg); /* subclasses must override this: return a bounding box for the display of the system given current parameters and energy levels */ abstract DoubleRectangle bounds(); double sq(double x) { return x*x; } void setInitialState(double q[], double qp[]) { for (int i=0; i=0; j--) { double b=v[c[j]]; for (int k=j+1; k0) ymin=0; else ymax=0; } return new DoubleRectangle(xmin,ymin,xmax,ymax).enlarge(.05); } // ---------------------------------------------------------------------- // Event handling void mouseMap(double xx, double yy) { map(0, xx); map(1, yy); } } /** a three-dimensional dynamical system */ class SpringPendulum extends MechSystem { // ---------------------------------------------------------------------- // internal variables: private double m0, m1, l0, l1, k0, g; // physical parameters private int rad0, rad1; // radii of masses // ---------------------------------------------------------------------- // provide the name of this type of MechSystem String getType() { return "SpringPendulum"; } // ---------------------------------------------------------------------- // provide information on the parameters: // name, description, units, default value Parameter[] getParamInfo() { Parameter[] paraminfo = { new Parameter("m0", "inner mass", "kg", 2), new Parameter("m1", "outer mass", "kg", 1), new Parameter("l0", "inner length", "m", 6), new Parameter("l1", "outer length", "m", 4), new Parameter("k0", "spring constant", "N/m", 60), new Parameter("g", "gravity", "m/s^2", 9.81), }; return paraminfo; } // ---------------------------------------------------------------------- // provide information on the coordinates: // default initial value, default initial velocity, // period (0 if not periodic), description, units Coordinate[] getCoordInfo() { Coordinate[] coordinfo = { new Coordinate("x", "coordinate of inner mass, right of pivot", "m", 2.2, 0, 0), new Coordinate("y", "coordinate of inner mass, above pivot", "m", 3, 0, 0), new Coordinate("alpha", "angle of outer leg, counterclockwise from south", "radians", 1, 0, 2*Math.PI), }; return coordinfo; } // ---------------------------------------------------------------------- // initialize the internal parameters from the param[] array, and // calculate any auxiliary values void setphysics() { this.m0=param[0]; this.m1=param[1]; this.l0=param[2]; this.l1=param[3]; this.k0=param[4]; this.g=param[5]; rad0 = (int)(5*Math.sqrt(m0/Math.max(m0,m1))); rad1 = (int)(5*Math.sqrt(m1/Math.max(m0,m1))); } // ---------------------------------------------------------------------- // return the kinetic energy of the current state. The coordinates // and their first derivatives are available in q[] and qp[], respectively double T() { return (m0+m1)/2 * (sq(qp[0])+sq(qp[1])) + m1/2 * sq(l1)*sq(qp[2]) + m1 * qp[2]*l1*(qp[0]*Math.cos(q[2])+qp[1]*Math.sin(q[2])); } // ---------------------------------------------------------------------- // return the potential energy of the current state. The coordinates // and their first derivatives are available in q[] and qp[], respectively double U() { return k0/2 * sq(Math.sqrt(sq(q[0])+sq(q[1]))-l0) + (m0+m1)*g * q[1] - m1*g*l1*Math.cos(q[2]); } // ---------------------------------------------------------------------- // draw the current state. May call the routines drawSpring, // drawLine, drawMass, drawCircle, and recordTrace, whose // coordinates are in meters. Any other drawing methods on the // Graphics objects may also be called, but the coordinates must be // converted, for instance og.drawRectangle(c.x(left), c.y(top), // c.x(right)-c.x(left), c.x(bot)-c.x(top)). void drawsystem(Graphics og, Graphics bg) { double x0 = q[0]; double y0 = q[1]; double x1 = x0+l1*Math.sin(q[2]); double y1 = y0-l1*Math.cos(q[2]); recordTrace(bg,og,x1,y1); drawSpring(og,0,0,x0,y0,15,0.2); drawLine(og,x0,y0,x1,y1); drawMass(og,x0,y0,rad0); drawMass(og,x1,y1,rad1); } // ---------------------------------------------------------------------- // given the current state and energy, return a rectangle in which // the image drawn by the drawsystem routine will fit, even as the // system evolves (assuming conservation of energy). The // coordinates of the rectangle are physical coordinates (in // meters), not screen coordinates. They are the same coordinates // the drawing routines use. DoubleRectangle bounds() { double l=l1+l0+ Math.sqrt(sq((m0+m1)*g/k0)+2*l0*(m0+m1)*g/k0+2*m1*g/k0+2*E()/k0); double equi=(m0+m1)*g/k0; return new DoubleRectangle(-l,-equi-l,l,-equi+l).enlarge(.05); } // ---------------------------------------------------------------------- // Event handling. When the mouse goes down on physical coordinates // (xx,yy), down(xx,yy) is called; similar for drag and up. Call // fix(i,v) to fix the ith coordinate to value v (the dynamical // system will then behave as if these coordinate had been // constrained). Call movecoord(i,v) to change the value of an // already fixed coordinate. Call release(i) to release the ith // coordinate; it will then become dynamic again. void mouseMap(double xx, double yy) { map(0, xx); map(1, yy); } } /************************************************************************ ************************************************************************ ************************************************************************/ /** a three-dimensional dynamical system */ class SwingingSpring extends MechSystem { // ---------------------------------------------------------------------- // internal variables: private double m0, l0, k0, g; // physical parameters private double l; // length at equilibrium private double U0; // Pot. En. at equilibrium private int rad00, rad0; // radii of pivot and bob private double tauR, tauZ; // swinging and springing periods // ---------------------------------------------------------------------- // provide the name of this type of MechSystem String getType() { return "SwingingSpring"; } // ---------------------------------------------------------------------- // provide information on the parameters: // name, description, units, default value Parameter[] getParamInfo() { Parameter[] paraminfo = { new Parameter("m0", "mass", "kg", 1.0), new Parameter("l0", "length", "m", 0.75), new Parameter("k0", "spring constant", "N/m", 4*Math.PI*Math.PI), new Parameter("g", "gravity", "m/s^2", Math.PI*Math.PI), }; return paraminfo; } // ---------------------------------------------------------------------- // provide information on the coordinates: // default initial value, default initial velocity, // period (0 if not periodic), description, units Coordinate[] getCoordInfo() { Coordinate[] coordinfo = { new Coordinate("x", "coordinate of mass, right of pivot", "m", 0.04, 0, 0), new Coordinate("y", "coordinate of mass, into screen", "m", 0, 0.034272, 0), new Coordinate("z", "coordinate of mass, above pivot", "m", -1+0.08, 0, 0), }; return coordinfo; } // ---------------------------------------------------------------------- // initialize the internal parameters from the param[] array, and // calculate any auxiliary values void setphysics() { this.m0=param[0]; this.l0=param[1]; this.k0=param[2]; this.g=param[3]; rad0 = (int)(10.0); rad00 = (int)(4.0); l = l0 + m0*g/k0; U0 = (k0/2)*sq(l-l0) + m0*g*(-l); tauR = 2*Math.PI* Math.sqrt(l/g); tauZ = 2*Math.PI* Math.sqrt(m0/k0); } // ---------------------------------------------------------------------- // return the kinetic energy of the current state. The coordinates // and their first derivatives are available in q[] and qp[], respectively double T() { return (m0/2)*( sq(qp[0]) + sq(qp[1]) + sq(qp[2]) ); } // ---------------------------------------------------------------------- // return the potential energy of the current state. The coordinates // and their first derivatives are available in q[] and qp[], respectively. // U is defined so that the potential energy at equilibrium vanishes. double U() { return (k0/2)*sq(Math.sqrt( sq(q[0]) + sq(q[1]) + sq(q[2]) ) - l0) + m0*g*q[2] - U0; } // ---------------------------------------------------------------------- // draw the current state. May call the routines drawSpring, // drawLine, drawMass, drawPivot, and recordTrace, whose // coordinates are in meters. Any other drawing methods on the // Graphics objects may also be called, but the coordinates must be // converted, for instance og.drawRectangle(c.x(left), c.y(top), // c.x(right)-c.x(left), c.x(bot)-c.x(top)). void drawsystem(Graphics og, Graphics bg) { double zpmax = Math.sqrt( 2*E()/k0 ); double h = 2*zpmax; double x0 = q[0]; double y0 = q[1]; double z0 = q[2]; recordTrace(bg,og,x0,z0); int radbob = (int)(rad0*Math.max((1-y0/h),0.1)); drawSpring(og,0,0,x0,z0,100,0.1); drawPivot(og,0,0,rad00); drawMass(og,x0,z0,radbob); double wleft = 2*h; double wright = 2*h; double wcenter = 0.5*h; double woffset = wleft + wcenter; double xx0 = q[0]+woffset; double yy0 = q[1]-l; double zz0 = q[2]+l; recordSpoor(bg,og,xx0,yy0); int radbob2 = (int)(rad0*Math.max((1+zz0/h),0.1)); drawSpring(og,woffset,-l,xx0,yy0,30,0.1); drawPivot(og,woffset,-l,rad00); drawMass(og,xx0,yy0,radbob2); } // ---------------------------------------------------------------------- // given the current state and energy, return a rectangle in which // the image drawn by the drawsystem routine will fit, even as the // system evolves (assuming conservation of energy). The // coordinates of the rectangle are physical coordinates (in // meters), not screen coordinates. They are the same coordinates // the drawing routines use. DoubleRectangle bounds() { double zpmax = Math.sqrt( 2*E()/k0 ); double h = 2*zpmax; double wleft = 2*h; double wright = 2*h; double wcenter = 0.5*h; double w = wleft + wcenter + wright; return new DoubleRectangle(-h,-l-h,-h+w,-l+h).enlarge(.05); } // ---------------------------------------------------------------------- // Event handling. When the mouse goes down on physical coordinates // (xx,zz), down(xx,zz) is called; similar for drag and up. Call // fix(i,v) to fix the ith coordinate to value v (the dynamical // system will then behave as if these coordinate had been // constrained). Call movecoord(i,v) to change the value of an // already fixed coordinate. Call release(i) to release the ith // coordinate; it will then become dynamic again. /* void mouseMap(double xx, double zz) { map(0, xx); map(2, zz); } */ } /************************************************************************ ************************************************************************ ************************************************************************/ /** a class that keeps track of a coordinate system, particularly the relationship between screen coordinates (in pixels) and physical coordinates (in meters). It can also draw a scale (i.e. a virtual yardstick) of itself. */ class CoordinateSystem { int originx, originy; // the coordinates of the origin, in pixels double unitx, unity; // the unit length, in pixels, as a double int exp, mant; int scalepixels; String scalename; DoubleRectangle b; Dimension d; CoordinateSystem(Dimension d, DoubleRectangle b) { this.d = d; this.b = b; // calculate units unitx=d.width/(b.right-b.left); unity=d.height/(b.top-b.bot); // constrain and orient units unitx=Math.min(unitx,unity); unity=-unitx; // calculate origin originx=(int)(d.width-(b.left+b.right)*unitx)/2; originy=(int)(d.height-(b.top+b.bot)*unity)/2; // calculate a scale for unit if (d.width>20) { double log10=Math.log((d.width-20)/unitx)/Math.log(10); exp=(int)Math.floor(log10); mant=(int)Math.floor(Math.exp((log10-exp)*Math.log(10))); if (mant<5) mant=1; else mant=5; scalepixels=(int)(mant*Math.exp(exp*Math.log(10))*unitx); } else exp=scalepixels=0; if (exp>=3) scalename=(mant*dexp(exp-3))+" km"; else if (exp>=0) scalename=(mant*dexp(exp-0))+" m"; else if (exp>=-2) scalename=(mant*dexp(exp+2))+" cm"; else if (exp>=-3) scalename=(mant*dexp(exp+3))+" mm"; else scalename=mant+"x10^"+exp+" m"; } int dexp(int n) { int y=1; for (int i=0; i