// Jostling.java - atoms drifting (or kids running) around in square // region - initially region is stationary and then region is moved // which causes static pressure to drop (or kids to collide more softly) // Copyright 2004 mark mitchell // No permission is granted for using this applet, or any derivative products, // for commercial purposes // 5/25/2004 written by mark mitchell // all time variables are in units of milliseconds // command line parameters are all listed in java console // to compile this and create a jar file for uploading you will need // the java development kit (jdk) from http:java.sun.com/j2se. then // perform the following steps: // 1) javac -target 1.1 Jostling.java // 2) jar cvf Jostling.jar *.class // 3) rm *.class // useful links: // // during stationary-to-movingtransition, the standard deviation of // the speeds starts out high and then settles down too slowly // to be visible in this demo (as verified using // the ConstClass.showOneSigma flag), so we reset the // velocities which skips the transition. // // Advantage: It seems to take minutes in this demo to // pass through the transition that would take // some small fraction of a second in a real // fluid, since atoms are so quick // Disadvantage: When the box starts moving, some atoms // will immediately change their direction // even though they are not in contact with // a wall or other atom import java.applet.*; import java.awt.*; import java.awt.event.*; import java.awt.Color.*; import java.lang.*; import java.lang.Math.*; import java.util.*; import java.net.URL; import java.text.DecimalFormat; // for debugging only class Atom { private Image atomImage; private double x, y; // x and y coordinates private double vx, vy; // x and y components of velocity private CollideAtomWall collide; public Atom(Random rnd, Image atomImage, double initialSpeed) { this.atomImage = atomImage; x = 0.0; y = 0.0; vx = 0.0; vy = 0.0; initializePositionVelocity(rnd, initialSpeed); collide = new CollideAtomWall(); } public void initializePositionVelocity(Random rnd, double initialSpeed) { // pick random location inside initial bounding box x = ConstClass.marginWidth + rnd.nextDouble() * (ConstClass.regionLength - ConstClass.marginWidth); y = ConstClass.marginWidth + rnd.nextDouble() * (ConstClass.regionLength - ConstClass.marginWidth); initializeVelocity(rnd, initialSpeed, 0.0); // DecimalFormat f = new DecimalFormat(" 00.000000; -00.000000"); // System.out.println("pv "+ // f.format(x)+","+ // f.format(y)+","+ // f.format(vx)+","+ // f.format(vy)); } public void initializeVelocity(Random rnd, double initialSpeed, double vxBoundingBox) { // pick arbitrary velocity direction, which will only be applied // to the part of the velocity that can have arbitrary direction double angle = 2.0 * Math.PI * rnd.nextDouble(); // portion of velocity that can be given arbitrary direction double iSq = initialSpeed * initialSpeed; double vSq = vxBoundingBox * vxBoundingBox; double s = Math.sin(angle); double radical = iSq - vSq * s * s; if (radical < 0.0) { double minInterval = (double) (ConstClass.appletWidth - ConstClass.regionLength) / initialSpeed; System.out.println("bounding box is moving too " + "quickly. increase panningStateInterval to " + "at least " + minInterval); System.exit(-1); } double vArbitrary; double c = Math.cos(angle); if (vxBoundingBox * c < 0) { vArbitrary = -Math.sqrt(radical) - vxBoundingBox * c; } else { vArbitrary = Math.sqrt(radical) - vxBoundingBox * c; } // combine arbitrary-direction and fixed-direction velocities. the // total kinetic energy is given only by initialSpeed, and not // vxBoundingBox (vx^2+vy^2=initialSpeed^2) vx = vArbitrary * Math.cos(angle) + vxBoundingBox; vy = vArbitrary * Math.sin(angle); } public void paint(Graphics g) { // draw atom offset so that center is at (x,y) g.drawImage(atomImage, (int) (x - ConstClass.atomRadius), (int) (y - ConstClass.atomRadius), null); } public double propagateOneTimestep(double vxBoundingBox, BoundingBox boundingBox, double deltaTime) { double momentumChange = collide.findExecuteCollisions(vxBoundingBox, boundingBox, this); x += vx * deltaTime; y += vy * deltaTime; return momentumChange; } public void setPosition(double x, double y) { this.x = x; this.y = y; } public void setVelocity(double vx, double vy) { this.vx = vx; this.vy = vy; } public double x() { return x; } public double y() { return y; } public double vx() { return vx; } public double vy() { return vy; } } class BoundingBox { // bounding box, which limits where atoms can travel, is either // stationary or panning to the right private double xBoundLow; // left private double yBoundLow; // top private double xBoundHigh; // right private double yBoundHigh; // bottom public BoundingBox() { initializePosition(); } public void initializePosition() { xBoundLow = ConstClass.marginWidth + 1; yBoundLow = ConstClass.marginWidth + 1; xBoundHigh = ConstClass.regionLength - ConstClass.marginWidth - 1; yBoundHigh = ConstClass.regionLength - ConstClass.marginWidth - 1; } public void paint(Graphics g) { int xPerimeter = (int) xBoundLow - ConstClass.marginWidth; g.drawRect(xPerimeter, 0, ConstClass.regionLength - 1, ConstClass.regionLength - 1); } public void propagateOneTimestep(double vxBoundingBox, double deltaTime) { double xDelta = vxBoundingBox * deltaTime; xBoundLow += xDelta; xBoundHigh += xDelta; } public double xBoundHigh() { return this.xBoundHigh; } public double xBoundLow() { return this.xBoundLow; } public double yBoundHigh() { return this.yBoundHigh; } public double yBoundLow() { return this.yBoundLow; } } class BoundingBoxState { private boolean traceSpew; private boolean resetPosition; private boolean resetVelocity; private double interval; private RegionPanel region; // reference to region private double vxBoundingBox; // total of momentum changes during interval gives pressure private double totalMomentumChange; public BoundingBoxState(boolean traceSpew, boolean resetPosition, boolean resetVelocity, RegionPanel region, double interval, double vxBoundingBox) { this.traceSpew = traceSpew; this.resetPosition = resetPosition; this.resetVelocity = resetVelocity; this.region = region; this.interval = interval; this.vxBoundingBox = vxBoundingBox; } public void dumpMomentum() { if (traceSpew && (interval > 0)) { DecimalFormat f = new DecimalFormat("0.0000000"); System.out.println("average momentum change rate " + f.format(totalMomentumChange / interval) + " [" + (int) interval + "]"); } } public void dumpSpeedOneSigma() { if (ConstClass.showOneSigma) { System.out.println("standard deviation of speed: " + region.speedOneSigma()); } } public double interval() { return interval; } public void reset(Random rnd, double initialSpeed) { totalMomentumChange = 0.0; region.reset(resetPosition, resetVelocity, rnd, initialSpeed, vxBoundingBox); } public void stepAnimation(double deltaTime) { totalMomentumChange += region.stepAnimation(vxBoundingBox, deltaTime); } } class CollideAtomAtom { private int atomDiameterSq = ConstClass.atomDiameter * ConstClass.atomDiameter; public double executeCollision(Atom atoms[], int i0, double x0, double y0, int i1, double x1, double y1, double x0To1, double y0To1) { // how do two spheres of equal mass behave in a collision? what // will the a and b components of velocities be? // // arbitrarily define the b direction to from one sphere center // to the other (which is radial to both spheres). then define // the a direction to be at a right angle (which is tangential to // both spheres) // // since the force and impact during the collision has no // tangential components, the a components are not affected. this // reduces the set of unknowns from four (va0,vb0,va1,vb1) to // two (vb0,vb1) // // there are two equations to solve for two unknowns: // 1) conservation of momentum in b direction // 2) conservation of energy // // there are two solutions // 1) both spheres continue along original trajectories, as // if there was never a collision. this is not acceptable // since at some point they will both exist in same place // at the same time // 2) both spheres continue along with their original b // velocity components, but they swap a velocity components double vx0I = atoms[i0].vx(); double vy0I = atoms[i0].vy(); double vx1I = atoms[i1].vx(); double vy1I = atoms[i1].vy(); // orthogonal unit vectors are (ax,ay) and (bx,by) double magnitude = Math.sqrt(x0To1 * x0To1 + y0To1 * y0To1); double ax = x0To1 / magnitude; double ay = y0To1 / magnitude; double bx = ay; double by = -ax; // project initial velocity vectors onto a and b unit vectors double va0I = vx0I * ax + vy0I * ay; double vb0I = vx0I * bx + vy0I * by; double va1I = vx1I * ax + vy1I * ay; double vb1I = vx1I * bx + vy1I * by; // final velocities have a components exchanged double va0F = va1I; double vb0F = vb0I; double va1F = va0I; double vb1F = vb1I; // invert the (a,b)=T (x,y) transform to get T(-1) double discriminant = ax * by - ay * bx; double xa = by / discriminant; double xb = -ay / discriminant; double ya = -bx / discriminant; double yb = ax / discriminant; // transform back to (x,y) double vx0F = va0F * xa + vb0F * xb; double vy0F = va0F * ya + vb0F * yb; double vx1F = va1F * xa + vb1F * xb; double vy1F = va1F * ya + vb1F * yb; // if (traceSpew) { // System.out.println("ax "+ax+" ay "+ay+" bx "+bx+" by "+by); // System.out.println("xyI "+vx0I+" "+vy0I+" "+vx1I+" "+vy1I); // System.out.println("abI "+va0I+" "+vb0I+" "+va1I+" "+vb1I); // System.out.println("abF "+va0F+" "+vb0F+" "+va1F+" "+vb1F); // System.out.println("xyF "+vx0F+" "+vy0F+" "+vx1F+" "+vy1F); // } atoms [i0].setVelocity(vx0F, vy0F); atoms [i1].setVelocity(vx1F, vy1F); // if (traceSpew) { // double energyXYI = // vx0I * vx0I + vy0I * vy0I + // vx1I * vx1I + vy1I * vy1I; // double energyABI = // va0I * va0I + vb0I * vb0I + // va1I * va1I + vb1I * vb1I; // double energyABF = // va0F * va0F + vb0F * vb0F + // va1F * va1F + vb1F * vb1F; // double energyXYF = // vx0F * vx0F + vy0F * vy0F + // vx1F * vx1F + vy1F * vy1F; // System.out.println("energies " + energyXYI + "->" + // energyABI + "->" + // energyABF + "->" + // energyXYF); // } // momentum change return Math.sqrt((vx0F - vx0I) * (vx0F - vx0I) + (vy0F - vy0I) * (vy0F - vy0I)); } public double findExecuteCollisions(Atom atoms[], int atomCount) { // search for collisions double momentumChange = 0.0; for (int i0 = 0 ; i0 < atomCount - 1; i0++) { double x0 = atoms[i0].x(); double y0 = atoms[i0].y(); double vx0 = atoms[i0].vx(); double vy0 = atoms[i0].vy(); for (int i1 = i0 + 1; i1 < atomCount; i1++) { double x1 = atoms[i1].x(); double y1 = atoms[i1].y(); double vx1 = atoms[i1].vx(); double vy1 = atoms[i1].vy(); // skip distance computation if there is no hope. this low // overhead test will apply in the vast majority of cases double x0To1 = x1 - x0; double y0To1 = y1 - y0; if (x0To1 > ConstClass.atomDiameter) continue; if (x0To1 < -ConstClass.atomDiameter) continue; if (y0To1 > ConstClass.atomDiameter) continue; if (y0To1 < -ConstClass.atomDiameter) continue; // if they have just collided then the separation distance is // increasing, so they should not be considered for collisions if (separationIsIncreasing(x0, y0, vx0, vy0, x1, y1, vx1, vy1)) continue; // compute distance squared double dSq = x0To1 * x0To1 + y0To1 * y0To1; if (dSq <= atomDiameterSq) { momentumChange += executeCollision(atoms, i0, x0, y0, i1, x1, y1, x0To1, y0To1); } } } return momentumChange; } private boolean separationIsIncreasing(double xA, double yA, double vxA, double vyA, double xB, double yB, double vxB, double vyB) { // separation vector is (RB-RA)+(VB-VA)*t, so the magnitude // of the separation is // // s=(s0^2 + 2(dx0 * dxdot0 + dy0 * dydot0)t + dv0^2 t^2)^0.5 // where // dx0 = xB - xA // dy0 = yB - yA // dxdot0 = vxB - vxA // dydot0 = vyB - vyA // s0 = (dx0^2 + dy0^2)^0.5 // dv0 = (dxdot0^2 + dydot0^2)^0.5 // // differentiating, we get sdot > 0 (s is increasing) at t=0 if and // only if dx0 * dxdot0 + dy0 * dydot0 > 0 return (xB - xA) * (vxB - vxA) + (yB - yA) * (vyB - vyA) > 0.0; } } class CollideAtomWall { public double findExecuteCollisions(double vx, BoundingBox boundingBox, Atom atom) { // if atom bounces off a moving wall, new velocity is affected // by that movement (think of reference frame in which the // moving wall is stationary) double xI = atom.x(); double yI = atom.y(); double vxI = atom.vx(); double vyI = atom.vy(); double vxF = rangeLimit(xI, vxI, vx, boundingBox.xBoundLow(), boundingBox.xBoundHigh()); double vyF = rangeLimit(yI, vyI, 0.0, boundingBox.yBoundLow(), boundingBox.yBoundHigh()); atom.setVelocity(vxF, vyF); return Math.sqrt((vxF - vxI) * (vxF - vxI) + (vyF - vyI) * (vyF - vyI)); } private double rangeLimit(double sAtom, double vAtom, double vBoundingBox, double sLow, double sHigh) { // velocity of atom in the reference frame of the wall. this // is positive if atom is approaching, or negative if leaving double vWallFrame; if (sAtom > sHigh) { // atom position is too high vWallFrame = vAtom - vBoundingBox; if (vWallFrame > 0) { return -(vWallFrame - vBoundingBox); } } else if (sAtom < sLow) { // atom position is too low vWallFrame = -vAtom + vBoundingBox; if (vWallFrame > 0) { return vWallFrame + vBoundingBox; } } // no collision occured return vAtom; } } class CommandLine { private Applet applet; public CommandLine(Applet applet) { this.applet = applet; } public boolean parseBoolean(String name, boolean dflt) { String valueField = applet.getParameter(name); if (valueField != null) { if (valueField.length() > 0) { boolean value = Boolean.valueOf(valueField).booleanValue(); System.out.println("Name: " + name + " Value: " + value); return value; } } System.out.println("Name: " + name + " Value: " + dflt + " (default)"); return dflt; } public double parseDouble(String name, double dflt) { String valueField = applet.getParameter(name); if (valueField != null) { if (valueField.length() > 0) { double value = Double.parseDouble(valueField); System.out.println("Name: " + name + " Value: " + value); return value; } } System.out.println("Name: " + name + " Value: " + dflt + " (default)"); return dflt; } public int parseInteger(String name, int dflt) { String valueField = applet.getParameter(name); if (valueField != null) { if (valueField.length() > 0) { int value = Integer.parseInt(valueField); System.out.println("Name: " + name + " Value: " + value); return value; } } System.out.println("Name: " + name + " Value: " + dflt + " (default)"); return dflt; } } interface ConstClass { // stuff for all panels public static final int appletWidth = 460; public static final int appletHeight = 60; public static final Color borderColor = Color.black; public static final int atomCountDefault = 36; public static final double initialSpeed = 24.0; // in pixels per second public static final Color backgroundColor = new Color(220, 220, 220); // atom image is a sphere with the specified diameter in pixels public static final String atomImageFile = new String ("img/flowMarker.gif"); public static final int atomDiameter = 7; public static final int atomRadius = (atomDiameter - 1) / 2; // square region containing atoms public static final int marginWidth = atomRadius; public static final int regionLength = appletHeight - 1; // copyright public static final String copyright = new String ("\1002004 mark mitchell http://home.earthlink.net/~mmc1919"); public static final int xCopyright = 3; public static final int yCopyright = appletHeight - 6; public static final Color copyrightColor = new Color(173, 173, 173); public static final Font legendFont = new Font("Helvetica", Font.PLAIN, 10); // animation flip flops between stationary and panning state public static final int wakeupInterval = 100; public static final double stationaryStateInterval = 5000; public static final double panningStateInterval = 20000; // units public static final double sec2Millisec = 1.0 / 1000.0; // transition from stationary to moving public static final boolean skipTransition = false; public static final boolean showOneSigma = true; // this causes jitter! this overrides traceSpew! public static final long oneSigmaInterval = 1000; // in milliseconds } public class Jostling extends Applet implements Runnable { // panels, from top to bottom RegionPanel region; // animation Thread animationThread; Image atomImage; // debug boolean traceSpew; BoundingBoxState stateStationary, statePanning; // possible states BoundingBoxState stateCurrent; // reference to possible state Random rnd; double initialSpeed; public void init() { setLayout(null); System.out.println(""); System.out.println("Copyright 2004, Mark Mitchell"); System.out.println("http://home.earthlink.net/~mmc1919"); CommandLine cmdLine = new CommandLine(this); // java applet will run at this priority int priority = cmdLine.parseInteger("priority", Thread.MIN_PRIORITY); // true to turn on trace spew traceSpew = cmdLine.parseBoolean("traceSpew", false); // true to turn on copyright boolean showCopyright = cmdLine.parseBoolean("showCopyright", false); // number of atoms int atomCount = cmdLine.parseInteger("atomCount", ConstClass.atomCountDefault); // initial atom speed initialSpeed = ConstClass.sec2Millisec * cmdLine.parseDouble("initialSpeed", ConstClass.initialSpeed); atomImage = getImage(getCodeBase(), ConstClass.atomImageFile); if (atomImage == null) { System.out.println("Unable to read image"); System.exit(-1); } // use global randomizer since transient randomizers exhibit // obvious correlations from one randomizer to the next rnd = new Random(); // panels region = new RegionPanel (this, traceSpew, showCopyright, atomCount, initialSpeed, atomImage, rnd); add(region); // bounding box speed is computed so bounding box just crosses // the applet during the time in that state double vxBoundingBox = (double) (ConstClass.appletWidth - ConstClass.regionLength) / ConstClass.panningStateInterval; // different states of state pattern take turns running everything stateStationary = new BoundingBoxState(traceSpew, true, true, region, ConstClass.stationaryStateInterval, 0.0); statePanning = new BoundingBoxState(traceSpew, false, ConstClass.skipTransition, region, ConstClass.panningStateInterval, vxBoundingBox); stateCurrent = stateStationary; // start animation thread animationThread = new Thread(this); animationThread.setPriority(priority); animationThread.start(); } public void run() { // units of time are clock ticks, which are one millisecond each long lastTime = System.currentTimeMillis(); long deltaTime = 0; long timeInState = 0; long timeInOneSigma = 0; while (true) { // sleep try { Thread.sleep(ConstClass.wakeupInterval); } catch (InterruptedException e) { } // update elapsed time long thisTime = System.currentTimeMillis(); deltaTime = thisTime - lastTime; lastTime = thisTime; // propagate ahead in time stateCurrent.stepAnimation(deltaTime); // show standard deviation of speed if appropriate timeInOneSigma += deltaTime; if (timeInOneSigma > ConstClass.oneSigmaInterval) { timeInOneSigma = 0; stateCurrent.dumpSpeedOneSigma(); } // switch states if appropriate timeInState += deltaTime; if (timeInState > stateCurrent.interval()) { timeInState = 0; stateCurrent.dumpMomentum(); if (stateCurrent == stateStationary) { stateCurrent = statePanning; } else { stateCurrent = stateStationary; } stateCurrent.reset(rnd, initialSpeed); } } } public void stop() { animationThread = null; } } class RegionPanel extends Panel { private Jostling jostling; private boolean traceSpew; // true to turn on trace spew private boolean showCopyright; // true to show copyright (a bit obnoxious) private Image atomImage; private int atomCount; // region containing atoms in graph coords. this might be moving private BoundingBox boundingBox; private Button btnPlay; private Atom atoms[]; // collision functions private CollideAtomAtom collide; // double buffering required since awt (unlike swing) does not have // built-in support for it Image offScreen; Graphics gOff; public void paint(Graphics g) { // moving the code in update() to here and deleting // that function, causes horrible flicker during // initial loading, and complete panel erase followed // by reloading of all three background images. this // approach gives no initial flicker, and causes // redraw of only the appropriate background image update(g); } private void paintCopyright(Graphics g) { g.setFont(ConstClass.legendFont); g.setColor(ConstClass.copyrightColor); g.drawString(ConstClass.copyright, ConstClass.xCopyright, ConstClass.yCopyright); } public RegionPanel(Jostling jostling, boolean traceSpew, boolean showCopyright, int atomCount, double initialSpeed, Image atomImage, Random rnd) { boundingBox = new BoundingBox(); this.jostling = jostling; this.traceSpew = traceSpew; this.showCopyright = showCopyright; this.atomCount = atomCount; this.atomImage = atomImage; setLayout(null); setBounds(0, 0, ConstClass.appletWidth, ConstClass.appletHeight); setBackground(ConstClass.backgroundColor); // create atoms atoms = new Atom [atomCount]; for (int i = 0; i < this.atomCount; i++) { atoms [i] = new Atom(rnd, atomImage, initialSpeed); } collide = new CollideAtomAtom(); } public void reset(boolean resetPosition, boolean resetVelocity, Random rnd, double initialSpeed, double vxBoundingBox) { if (ConstClass.showOneSigma) { System.out.println("Initializing velocity in stationary to moving transition? " + (resetVelocity ? "no" : "yes")); } for (int i = 0; i < atomCount; i++) { if (resetPosition && resetVelocity) { atoms [i].initializePositionVelocity(rnd, initialSpeed); } else { if (resetVelocity) { atoms [i].initializeVelocity(rnd, initialSpeed, vxBoundingBox); } } } boundingBox.initializePosition(); } public double speedOneSigma() { if (atomCount < 2) { return 0.0; } double sumV = 0.0; double sumVSquared = 0.0; for (int i = 0; i < atomCount; i++) { double v = Math.sqrt(atoms [i].vx() * atoms [i].vx() + atoms [i].vy() * atoms [i].vy()); sumV += v; sumVSquared += v * v; } double vAverage = sumV / atomCount; return Math.sqrt((sumV - atomCount * vAverage * vAverage) / (atomCount - 1.0)); } public double stepAnimation(double vxBoundingBox, double deltaTime) { // propagate atom trajectories double momentumChange = 0.0; for (int i = 0; i < atomCount; i++) { momentumChange += atoms [i].propagateOneTimestep(vxBoundingBox, boundingBox, deltaTime); } boundingBox.propagateOneTimestep(vxBoundingBox, deltaTime); momentumChange += collide.findExecuteCollisions(atoms, atomCount); repaint(); return momentumChange; } public void update(Graphics g) { // double buffering if (offScreen == null) { offScreen = createImage(ConstClass.appletWidth, ConstClass.appletHeight); gOff = offScreen.getGraphics(); } gOff.clearRect(0, 0, ConstClass.appletWidth, ConstClass.appletHeight); if (showCopyright) paintCopyright(gOff); for (int i = 0; i < atomCount; i++) { atoms [i].paint(gOff); } boundingBox.paint(gOff); offScreen.flush(); g.drawImage(offScreen, 0, 0, null); } }