/* Lagrange.java: a Java applet for animating finite dimensional * dynamical systems. Based on Lagrangian mechanics. * Author: Peter Selinger. * Copyright (c) 1999-2001 Peter Selinger. You are free to copy, * distribute, and modify this software under the terms of the GNU * General Public License. Appropriate credit must be given to the * original author and subsequent authors. This copyright notice may * not be removed or altered. For details, see www.gnu.org. * Changes: * 06/03/2001: fixed a bug in the MechSystem.solve() method pointed * out by Mehrtash Babadi. I added analytic differentiation, 2007/Oct, Norman Hardy */ /* 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 */ 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.14"; // 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 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?" }, { "controls", "boolean", "show controls?" }, { "showenergy", "boolean", "display total energy?" }, { "constrainenergy", "boolean", "enforce energy perservation?" }, { "energycontrol", "boolean", "show energy control button?" }, { "bgcolor", "#xxxxxx", "background color" }, { "fgcolor", "#xxxxxx", "foreground color" }, { "tracecolor", "#xxxxxx", "color of the trace" }, { "scalecolor", "#xxxxxx", "color of the metric scale" }, { "timecolor", "#xxxxxx", "color of the time scale" }, { "energycolor", "#xxxxxx", "color of the energy display" }, }; Vector infov = new Vector(); for (int i=0; i 3) dt = 3*idealdt; // advance the simulation by dt and repaint mechsys.step(dt); systemcanvas.repaint(1L); } catch (InterruptedException e) { stop(); } } } } /** a panel for three buttons optionally displayed in the main applet window */ class ControlPanel extends Panel implements WindowListener { MechSystem mechsys; SystemCanvas systemcanvas; Lagrange lagr; Button restartbutton, tracebutton, changebutton, energybutton, startstopbutton; ControlPanel(MechSystem mechsys, SystemCanvas systemcanvas, Lagrange lagr) { this.mechsys = mechsys; this.systemcanvas = systemcanvas; this.lagr = lagr; setBackground(Global.bgcolor); // create components restartbutton = new Button("Restart"); tracebutton = new Button("Tracing On/Off"); startstopbutton = new Button("Pause/Resume"); if (Global.energycontrol) { energybutton = new Button("Constrain Energy"); } changebutton = new Button("Change Parameters"); add(restartbutton); add(tracebutton); add(startstopbutton); if (Global.energycontrol) { add(energybutton); } 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 == tracebutton) { return systemcanvas.traceaction(); } if (target == startstopbutton) { return lagr.startstopaction(); } if (Global.energycontrol && target == energybutton) { return lagr.energyaction(); } 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; mechsys.adjustEnergy(); } catch (NumberFormatException exc) {} } void updateentries() { for (int i=0; i 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; CoordinateSystem c; boolean scalerequest = true; // dynamic variables to hold the state double q[]; // coordinates q1, q2, q3... double qp[]; // their derivatives q1', q2', ... boolean constrained[]; // is the ith variable currently constrained? double oldq[], oldqp[]; // remember initial state double totalenergy; // workspace for stepping function double A[][]; double rhs[]; double qpp[]; // the second derivatives q1'', q2'', ... double qn[]; // requested value for constrained coordinates double K[][], Kp[][], qtmp[], qptmp[]; // Runge-Kutta temporary storage // 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 constrained coordinate constrained = new boolean[N]; K = new double[4][N]; // Runge-Kutta temporaries Kp = new double[4][N]; qtmp = new double[N]; qptmp = new double[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 drawsystem_wrapper(Graphics og, Graphics bg) { if (Global.showenergy) { og.setColor(Global.energycolor); FontMetrics fontmetrics = og.getFontMetrics(); int fontheight = fontmetrics.getHeight(); double e = E(); if (e>10 || e<-10) { e = ((int)(10*e))/10.0; } else { e = ((int)(1000*e))/1000.0; } og.drawString("Energy (J): "+e, 10, 10+3*fontheight+1); } drawsystem(og, bg); } /* 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[r[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(double q[], double qp[]) { 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(double q[]) { 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. void mouseMap(double xx, double yy) { map(0, xx); map(1, yy); } } /** 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