You are viewing outdated content for BUG. If you have a BUG Y.T. edition or 2.0 series device, please visit our updated wiki: http://wiki.buglabs.net



N-Body Simulation Tutorial

From BUG Wiki

Jump to: navigation, search

Contents

Overview

2-D Vector
2-D Vector

In this introductory tutorial we will write a program that simulates simple gravitational attraction on moving bodies. The tutorial will cover the BUG SDK, Dragonfly, as well as the LCD module APIs. This will include some basic AWT GUI programming and access to the on-board accelerometer present in the BUG LCD module.

Credit

This tutorial was developed in part using the code in the Princeton CS class COS 126 materials available here. The Princeton materials use a desktop Swing GUI, but we will be using a simpler AWT based interface.

Prerequisites

A Blank Canvas
A Blank Canvas

This tutorial assumes you have a working SDK installed on your computer, and you understand the basics of Java and Eclipse. For this tutorial, any desktop operating system should be suitable. The Virtual BUG (VBug) will be used throughout the tutorial so it isn't even necessary to have a BUG device. However the VBug does not have very interesting accelerometer data so step 3 won't be as interesting without a real BUG.

Resources

Step 1: Getting Started

Create Project

Let's create a BUG project, and call it SimApp. If you need help with creating a project refer to the Baby Monitor App Tutorial. We will not select any services in the second page of the wizard, so just click finish and the SDK will generate a basic BUG project. Open the Activator.java. Notice that it implements BundleActivator. This is our entry point into the OSGi runtime environment. The start() method in this class will be called by Concierge when our application is started. For general information about OSGi read this community post. For more developer information check out this tutorial. Before proceeding you should know something about OSGi bundles, services, and the ServiceTracker.

Setup a ServiceTracker

Starting with the template Activator.java, we'll create and open a ServiceTracker object when our bundle is activated. The ServiceTracker takes in a BundleContext (we have this), a Filter (we can make one of these), and a ServiceTrackerCustomizer (we'll make one). Here is the code that creates and opens the ServiceTracker:

package simapp;
 
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Filter;
import org.osgi.util.tracker.ServiceTracker;
 
import com.buglabs.bug.module.lcd.pub.IModuleDisplay;
import com.buglabs.util.ServiceFilterGenerator;
 
public class Activator implements BundleActivator {
 
	private ServiceTracker st;
 
	public void start(BundleContext context) throws Exception {
		//Define the services we need to run.
		String services[] = { IModuleDisplay.class.getName() };
 
		//Generate a Filter based on our services.
		Filter serviceFilter = ServiceFilterGenerator.generateServiceFilter(context, services);
 
		//Create and open the ServiceTracker
		st = new ServiceTracker(context, serviceFilter, new SimCustomizer(context));
		st.open();
	}
 
	public void stop(BundleContext context) throws Exception {
		st.close();		
	}
}

Now we need to create the SimCustomizer class. This class will handle changes that occur in the OSGi runtime, and turn parts of our application on or off based on the services that are available. Changes that would be important to our application would be the insertion or removal of the LCD module. In our Activator.Start() method we defined the service we are interested in as the IModuleDisplay service. This service gets registered in the OSGi runtime when a LCD module is attached. So, in SimCustomizer, we need to handle the case where we get access to such a service, and handle the case where we lose the service.

package simapp;
 
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceReference;
import org.osgi.util.tracker.ServiceTrackerCustomizer;
 
import com.buglabs.bug.module.lcd.pub.IModuleDisplay;
 
public class SimCustomizer implements ServiceTrackerCustomizer {
 
	private final BundleContext context;
 
	public SimCustomizer(BundleContext context) {
		this.context = context;
	}
 
	public Object addingService(ServiceReference reference) {
		Object svcObj = context.getService(reference);
 
		//We now have a reference to our LCD display service.
		IModuleDisplay display = (IModuleDisplay) svcObj;
 
		return svcObj;
	}
 
	public void modifiedService(ServiceReference reference, Object service) {
	}
 
	public void removedService(ServiceReference reference, Object service) {
		//This means our display service has gone away.  Probably the LCD Module was removed.
	}
}

Open AWT Window

Alright, well our SimCustomizer app gets a reference to the LCD display, but we need to do something with it! Since we're making a simulation, let's just create a window for now. We don't even need to add any widgets to it. Here is the new SimCustomizer that creates a window when the LCD module is available:

package simapp;
 
import java.awt.Frame;
 
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceReference;
import org.osgi.util.tracker.ServiceTrackerCustomizer;
 
import com.buglabs.bug.module.lcd.pub.IModuleDisplay;
 
public class SimCustomizer implements ServiceTrackerCustomizer {
 
	private final BundleContext context;
	private Frame frame;
 
	public SimCustomizer(BundleContext context) {
		this.context = context;
	}
 
	public Object addingService(ServiceReference reference) {
		Object svcObj = context.getService(reference);
 
		//We now have a reference to our LCD display service.
		IModuleDisplay display = (IModuleDisplay) svcObj;
 
		frame = display.getFrame();
                frame.setTitle("SimApp");
		frame.show();
 
		return svcObj;
	}
 
	public void modifiedService(ServiceReference reference, Object service) {
	}
 
	public void removedService(ServiceReference reference, Object service) {
		//This means our display service has gone away.  Probably the LCD Module was removed.
		if (frame != null) {
			frame.dispose();
			frame = null;
		}
	}
}

Update the OSGi Manifest

We need to do one last thing before we can run our application in the Virtual Bug: update the OSGi manifest such that we define all the external classes we need. Per the spec, any classes in the java.* and org.osgi.framework.* namespaces are automatically imported, so we only need to handle the other cases. Looking at our two classes there are three package namespaces we'll need to import: com.buglabs.bug.module.lcd.pub, org.osgi.util.tracker, com.buglabs.util. So, here is the final META-INF/MANIFEST.MF file:

Manifest-Version: 1.0
Bundle-Name: SimApp
Bundle-Activator: simapp.Activator
Bundle-SymbolicName: SimApp
Bundle-Version: 1.0.0
Bundle-Vendor: kgilmer
Bug-Bundle-Type: Application
Import-Package: com.buglabs.bug.module.lcd.pub, org.osgi.util.tracker, com.buglabs.util

Run the App

You should now start the Virtual BUG. The SDK will automatically pick up our application and start it for us. If everything goes as expected, you should see something like this:

Image:Eclipse-blank-UI.png

Remember to attach the virtual LCD module to the BUG. The small window to the bottom right is our application!

Step 2: Adding the Simulation Code

Now that we have a basic window BUG app going, let's add our simulation code. We won't dive into the details of the math or physics involved, but it's a good idea to at least skim this and this. The code for this app is available as a BUGnet app, here. Rather than include all the code here I'll just summarize the classes that are purely for the simulation:

Vector.java

Vector math operations. Used for calculating positions of bodies in the simulation.

/*
 * This class is based on the class work for Princeton class COS126.  Material found here:
 * http://www.cs.princeton.edu/courses/archive/spr01/cs126/assignments/nbody.html
 * 
 */
package simapp;
 
/**
 * Here lies all the math I've forgotten over the years but am happy is available on-line.
 *
 */
public class Vector {
    private final int N;         // length of the vector
    private double[] data;       // array of vector's components
 
 
    // create the zero vector of length n
    public Vector(int N) {
        this.N    = N;
        this.data = new double[N];
    }
 
    // create a vector from the array d
    public Vector(double[] d) {
        N = d.length;
 
        // defensive copy so that client can't alter our copy of data[]
        data = new double[N];
        for (int i = 0; i < N; i++) {
            data[i] = d[i];
        }
    }
 
    // return the inner product of this Vector a and b
    public double dot(Vector b) {
        Vector a = this;
        if (a.N != b.N)  { throw new RuntimeException("Dimensions don't agree"); }
        double sum = 0.0;
        for (int i = 0; i < N; i++) {
            sum = sum + (a.data[i] * b.data[i]);
        }
        return sum;
    }
 
    // return the Euclidean norm of this Vector a
    public double magnitude() {
        Vector a = this;
        return Math.sqrt(a.dot(a));
    }
 
    // return the corresponding unit vector
    public Vector direction() {
        Vector a = this;
        return a.times(1.0 / a.magnitude());
    }
 
    // return a + b
    public Vector plus(Vector b) {
        Vector a = this;
        if (a.N != b.N) { throw new RuntimeException("Dimensions don't agree"); }
        Vector c = new Vector(N);
        for (int i = 0; i < N; i++) {
            c.data[i] = a.data[i] + b.data[i];
        }
        return c;
    }
 
    // return a - b
    public Vector minus(Vector b) {
        Vector a = this;
        if (a.N != b.N) { throw new RuntimeException("Dimensions don't agree"); }
        Vector c = new Vector(N);
        for (int i = 0; i < N; i++) {
            c.data[i] = a.data[i] - b.data[i];
        }
        return c;
    }
 
    // create and return a new object whose value is (this * factor)
    public Vector times(double factor) {
        Vector c = new Vector(N);
        for (int i = 0; i < N; i++) {
            c.data[i] = factor * data[i];
        }
        return c;
    }
 
    // return the corresponding unit vector
    public double cartesian(int i) {
        return data[i];
    }
 
    // return a string representation of the vector
    public String toString() {
        String s = "( ";
        for (int i = 0; i < N; i++) {
            s = s + data[i] + " ";
        }
        return s + ")";
    }
}

Body.java

Represents each discrete body that is being simulated.

/*
 * This class is based on the class work for Princeton class COS126.  Material found here:
 * http://www.cs.princeton.edu/courses/archive/spr01/cs126/assignments/nbody.html
 * 
 */
package simapp;
 
 
/**
 * Represents a body in the simulation that has mass, position, etc.
 *
 */
public class Body {
    private Vector r;      // position
    private Vector v;      // velocity
    private double mass;   // mass
	private double radius;
 
    public Body(Vector r, Vector v, double mass) {
        this.r = r;
        this.v = v;
        this.mass = mass;
        this.radius = (0.025 / 1.5E30) * mass;
    }
 
    public void move(Vector f, double dt) {
        Vector a = f.times(1/mass);
        v = v.plus(a.times(dt));
        r = r.plus(v.times(dt));
    }
 
    public Vector forceTo(Body b) {
        Body a = this;
        double G = 6.67e-11;
        Vector delta = a.r.minus(b.r);
        double dist = delta.magnitude();
        double F = (G * a.mass * b.mass) / (dist * dist);
        return delta.direction().times(F);
    }
 
    public double getRadius() {
    	return radius;
    }
 
    public double getX() {
    	return r.cartesian(0);
    }
 
    public double getY() {
    	return r.cartesian(1);
    }
 
}

Seed.java

A set of initial conditions by which to start the simulation.

package simapp;
 
import java.util.List;
 
/**
 * Represents the initial state of a Universe.
 * @author kgilmer
 *
 */
public class Seed {
	private List bodies;
	private double radius;
	private double timeDelta;
 
	public Seed(double radius, double timeDelta, List bodies) {
		this.radius = radius;
		this.bodies = bodies;		
		this.timeDelta = timeDelta;
	}
 
	public List getBodies() {
		return bodies;
	}
 
	public double getRadius() {
		return radius;
	}
 
	public double getTimeDelta() {
		return timeDelta;
	}
}

SeedStore.java

A place to get predefined Simulation seeds.

package simapp;
 
import java.util.ArrayList;
import java.util.List;
 
/**
 * A place to get preset universe seeds.
 * @author kgilmer
 *
 */
public class SeedStore {
	public static Seed getStable2BodySeed() {
		List bodies = new ArrayList();
 
		double[] position = { 0.0e00, 4.5e10 };
		double[] velocity = { 1.0e04, 0.0e00 };
		bodies.add(new Body(new Vector(position), new Vector(velocity), 1.5e30));
 
		position[0] = 0.0e00;
		position[1] = -4.5e10;
		velocity[0] = -1.0e04;
		velocity[1] = 0.0e00;
		bodies.add(new Body(new Vector(position), new Vector(velocity), 1.5e30));
 
		return new Seed(5.0e10, 10000, bodies);
	}
 
	public static Seed getUnstable2BodySeed() {
		List bodies = new ArrayList();
 
		double[] position = { 0.0e00, 4.5e10 };
		double[] velocity = { 1.0e04, 0.0e00 };
		bodies.add(new Body(new Vector(position), new Vector(velocity), 1.5e30));
 
		position[0] = 0.0e00;
		position[1] = -4.5e10;
		velocity[0] = -1.0e04;
		velocity[1] = 0.0e00;
		bodies.add(new Body(new Vector(position), new Vector(velocity), 1.1e30));
 
		return new Seed(5.0e10, 10000, bodies);
	}
 
	public static Seed getUnstable3BodySeed() {
		List bodies = new ArrayList();
 
		double[] position = { 0.0e00, 4.5e10 };
		double[] velocity = { 1.0e04, 0.0e00 };
		bodies.add(new Body(new Vector(position), new Vector(velocity), 1.5e30));
 
		position[0] = 0.0e00;
		position[1] = -4.5e10;
		velocity[0] = -1.0e04;
		velocity[1] = 0.0e00;
		bodies.add(new Body(new Vector(position), new Vector(velocity), 1.1e30));
 
		position[0] = 0.0e00;
		position[1] = -2.5e10;
		velocity[0] = -1.3e04;
		velocity[1] = 0.3e03;
		bodies.add(new Body(new Vector(position), new Vector(velocity), 1.0e30));
 
		return new Seed(5.0e10, 10000, bodies);
	}
}

SimulationCanvas.java

A subclassed AWT canvas class with some helper methods for dealing with Bodies.

package simapp;
 
import java.awt.Canvas;
import java.awt.Graphics;
 
/**
 * A canvas for displaying N-bodies, and some math for coord conversion.
 * @author kgilmer
 *
 */
public class SimulationCanvas extends Canvas {
	private static final long serialVersionUID = 4000638109922111560L;
	private double xmin;
	private double xmax;
	private double ymin;
	private double ymax;
	private Graphics graphics;
	private int penRadius;
	private final int width;
	private final int height;
	private static final double BORDER = 0.09;
	private static final int DEFAULT_SIZE = 512;
 
	public SimulationCanvas(int width, int height) {
		this.width = width;
		this.height = height;
		graphics = this.getGraphics();
 
	}
 
	public void setXscale(double min, double max) {
		double size = max - min;
		xmin = min - BORDER * size;
		xmax = max + BORDER * size;
	}
 
	/**
	 * Set the y-scale (a 10% border is added to the values).
	 * 
	 * @param min
	 *            the minimum value of the y-scale
	 * @param max
	 *            the maximum value of the y-scale
	 */
	public void setYscale(double min, double max) {
		double size = max - min;
		ymin = min - BORDER * size;
		ymax = max + BORDER * size;
	}
 
	public void clear() {
		if (graphics == null) {
			graphics = this.getGraphics();
		}
 
		graphics.clearRect(0, 0, width, height);
	}
 
	public void setPenRadius(double r) {
        if (r < 0) throw new RuntimeException("pen radius must be positive");
 
        this.penRadius = (int) (r * DEFAULT_SIZE);     
    }
 
	public void point(double x, double y) {
		graphics.fillOval((int) scaleX(x), (int) scaleY(y), penRadius, penRadius);	
	}
 
	private double scaleX(double x) {
		return width * (x - xmin) / (xmax - xmin);
	}
 
	private double scaleY(double y) {
		return height * (ymax - y) / (ymax - ymin) - 10;
	}
}

Universe.java

The class that holds it all together. This is also a thread that moves the state of the simulation through time.

/*
 * This class is based on the class work for Princeton class COS126.  Material found here:
 * http://www.cs.princeton.edu/courses/archive/spr01/cs126/assignments/nbody.html
 * 
 */
 
package simapp;
 
/**
 * Represents the app thread and the core keeper of state in the app.
 *
 */
public class Universe extends Thread {
	private final double radius; // radius of universe
	private final int N; // number of bodies
	private final Body[] orbs; // array of N bodies
	private final SimulationCanvas canvasAdapter;
	private final double dt;
	private boolean running = false;
 
	// read universe from standard input
	public Universe(SimulationCanvas canvasAdapter, Seed seed) {
		this.canvasAdapter = canvasAdapter;
		this.dt = seed.getTimeDelta();
 
		N = seed.getBodies().size();
 
		// the set scale for drawing on screen
		radius = seed.getRadius();
		canvasAdapter.setXscale(-radius, +radius);
		canvasAdapter.setYscale(-radius, +radius);
 
		// read in the N bodies
		orbs = (Body []) seed.getBodies().toArray(new Body[N]);
	}
 
	// increment time by dt units, assume forces are constant in given interval
	public void increaseTime(double dt) {
 
		// initialize the forces to zero
		Vector[] f = new Vector[N];
		for (int i = 0; i < N; i++) {
			f[i] = new Vector(new double[2]);
		}
 
		// compute the forces
		for (int i = 0; i < N; i++) {
			for (int j = 0; j < N; j++) {
				if (i != j) {
					f[i] = f[i].plus(orbs[j].forceTo(orbs[i]));
				}
			}
		}
 
		// move the bodies
		for (int i = 0; i < N; i++) {
			orbs[i].move(f[i], dt);
		}
	}
 
	private void renderBodies() {
		for (int i = 0; i < N; i++) {
			canvasAdapter.setPenRadius(orbs[i].getRadius());
	        canvasAdapter.point(orbs[i].getX(), orbs[i].getY());
		}
	}
 
	public void run() {
		running = true;
		try {
			while (running) {
				canvasAdapter.clear();
				this.increaseTime(dt);
				this.renderBodies();
				Thread.sleep(20);
			}
		} catch (InterruptedException e) {
		}
	}
 
	public void shutdown() {
		running = false;
	}
}

AppWindow.java

In step one we called IModuleDisplay.getFrame() and this created a window for us. Now we want to have greaterd control over the look of the window, and are going to subclass Frame as AppWindow. This class internally will create it's child controls and create a universe and get everything running.

package simapp;
 
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Frame;
import java.awt.Rectangle;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
 
/**
 * The window for displaying the N-Body simulation.  
 * 
 * This app is based on class materials from a Princeton CS course.  More details here:
 * http://www.cs.princeton.edu/courses/archive/spr01/cs126/assignments/nbody.html
 * @author kgilmer
 *
 */
public class AppWindow extends Frame implements WindowListener {
	private static final long serialVersionUID = -5976083685939581414L;
	private SimulationCanvas canvas;
	private Universe universe;
 
 
	public AppWindow() {
		super("SimApp");
		this.setBounds(new Rectangle(320, 240));
		this.setLayout(new BorderLayout());
		canvas = new SimulationCanvas(320, 240);
		canvas.setBackground(Color.BLACK);
		canvas.setForeground(Color.WHITE);
		this.add(canvas, BorderLayout.CENTER);
		this.addWindowListener(this);
	}
 
	/*
	 * (non-Javadoc)
	 * 
	 * @see java.awt.Window#show()
	 */
	public void show() {
		super.show();
 
		universe = new Universe(canvas, SeedStore.getStable2BodySeed());
		universe.start();
	}
 
	public void dispose() {
		if (universe != null) {
			universe.shutdown();
		}
 
		super.dispose();
	}
 
	public void windowActivated(WindowEvent arg0) {
	}
 
	public void windowClosed(WindowEvent arg0) {
	}
 
	public void windowClosing(WindowEvent arg0) {
		universe.shutdown();
		this.dispose();
	}
 
	public void windowDeactivated(WindowEvent arg0) {
	}
 
	public void windowDeiconified(WindowEvent arg0) {
	}
 
	public void windowIconified(WindowEvent arg0) {
	}
 
	public void windowOpened(WindowEvent arg0) {
	}
}

Running the App

Alright, after either creating the correct class files or downloading the finished app from BUGnet, you should be able to run the app. We only need to make one change, we need to create an instance of AppWindow when we know we have a reference to IModuleDisplay in SimCustomizer.java:

package simapp;
 
import java.awt.Frame;
 
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceReference;
import org.osgi.util.tracker.ServiceTrackerCustomizer;
 
import com.buglabs.bug.module.lcd.pub.IModuleDisplay;
 
public class SimCustomizer implements ServiceTrackerCustomizer {
 
	private final BundleContext context;
	private Frame frame;
 
	public SimCustomizer(BundleContext context) {
		this.context = context;
	}
 
	public Object addingService(ServiceReference reference) {
		Object svcObj = context.getService(reference);
 
		//We now have a reference to our LCD display service.
		IModuleDisplay display = (IModuleDisplay) svcObj;
 
		frame = new AppWindow();
		frame.setTitle("SimApp");
		frame.show();
 
		return svcObj;
	}
 
	public void modifiedService(ServiceReference reference, Object service) {
	}
 
	public void removedService(ServiceReference reference, Object service) {
		//This means our display service has gone away.  Probably the LCD Module was removed.
		if (frame != null) {
			frame.dispose();
			frame = null;
		}
	}
}

And finally, we can see our app runing in the Virtual BUG: Image:Simapp window running.png

Step 3: Integrating the Accelerometer

Well as you probably know, the BUG LCD module has an integrated accelerometer. Wouldn't it be fun to apply the forces reported by that device to our simulation? That would cause our simulated bodies to respond to real-world forces as well as computed forces.

Updating the ServiceTracker

The first thing we need is to get access to an Accelerometer device. There are a few APIs to choose from, and for this app, IAccelerometerSampleProvider will serve us nicely. First off is to add that service to our Filter so that when the service becomes available SimCustomizer is notified:

package simapp;
 
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Filter;
import org.osgi.util.tracker.ServiceTracker;
 
import com.buglabs.bug.accelerometer.pub.IAccelerometerSampleProvider;
import com.buglabs.bug.module.lcd.pub.IModuleDisplay;
import com.buglabs.util.ServiceFilterGenerator;
 
public class Activator implements BundleActivator {
 
	private ServiceTracker st;
 
	public void start(BundleContext context) throws Exception {
		//Define the services we need to run.
		String services[] = { IModuleDisplay.class.getName(), 
							  IAccelerometerSampleProvider.class.getName() };
 
		//Generate a Filter based on our services.
		Filter serviceFilter = ServiceFilterGenerator.generateServiceFilter(context, services);
 
		//Create and open the ServiceTracker
		st = new ServiceTracker(context, serviceFilter, new SimCustomizer(context));
		st.open();
	}
 
	public void stop(BundleContext context) throws Exception {
		st.close();		
	}
}

And now SimCustomizer gets a little more complicated. We are now tracking two services, and only want to start our simulation when both are available. This is arbitrary, as the code could also be written such that if the accelerometer is not available for some reason, the simulation does not try to factor the data into the vector calculations. SimCustomizer:

package simapp;
 
import java.awt.Frame;
import java.io.IOException;
 
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceReference;
import org.osgi.util.tracker.ServiceTrackerCustomizer;
 
import com.buglabs.bug.accelerometer.pub.IAccelerometerSampleProvider;
import com.buglabs.bug.module.lcd.pub.IModuleDisplay;
 
public class SimCustomizer implements ServiceTrackerCustomizer {
 
	private final BundleContext context;
	private Frame frame;
	private IModuleDisplay display;
	private AccelerometerMonitor accelMonitor;
 
	public SimCustomizer(BundleContext context) {
		this.context = context;
	}
 
	public Object addingService(ServiceReference reference) {
		Object svcObj = context.getService(reference);
 
		if (svcObj instanceof IModuleDisplay) {
			//We now have a reference to our LCD display service.
			display = (IModuleDisplay) svcObj;
		}
 
		if (svcObj instanceof IAccelerometerSampleProvider) {
			IAccelerometerSampleProvider sampleProvider = (IAccelerometerSampleProvider) svcObj;
			try {
				accelMonitor = new AccelerometerMonitor(sampleProvider, 500);
				accelMonitor.start();
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}			
		}
 
		if (display != null && accelMonitor != null && frame == null) {
			frame = new AppWindow(accelMonitor);
			frame.setTitle("SimApp");
			frame.show();
		}
 
		return svcObj;
	}
 
	public void modifiedService(ServiceReference reference, Object service) {
	}
 
	public void removedService(ServiceReference reference, Object service) {
		//This means our display service has gone away.  Probably the LCD Module was removed.
		if (accelMonitor != null) {
			accelMonitor.shutdown();
			accelMonitor = null;
		}
 
		if (frame != null) {
			frame.dispose();
			frame = null;
		}
	}
}

Also, you'll need to modify AppWindow.java to reflect the new constructor call above, which takes an AcceleratorMonitor as an argument now:

package simapp;
 
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Frame;
import java.awt.Rectangle;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
 
/**
 * The window for displaying the N-Body simulation.  
 * 
 * This app is based on class materials from a Princeton CS course.  More details here:
 * http://www.cs.princeton.edu/courses/archive/spr01/cs126/assignments/nbody.html
 * @author kgilmer
 *
 */
public class AppWindow extends Frame implements WindowListener {
	private static final long serialVersionUID = -5976083685939581414L;
	private SimulationCanvas canvas;
	private Universe universe;
	private AccelerometerMonitor accelMonitor;
 
 
	public AppWindow(AccelerometerMonitor accelMonitor) {
		super("SimApp");
		this.accelMonitor = accelMonitor;
		this.setBounds(new Rectangle(320, 240));
		this.setLayout(new BorderLayout());
		canvas = new SimulationCanvas(320, 240);
		canvas.setBackground(Color.BLACK);
		canvas.setForeground(Color.WHITE);
		this.add(canvas, BorderLayout.CENTER);
		this.addWindowListener(this);
	}
 
	/*
	 * (non-Javadoc)
	 * 
	 * @see java.awt.Window#show()
	 */
	public void show() {
		super.show();
 
		universe = new Universe(canvas, SeedStore.getStable2BodySeed(), accelMonitor);
		universe.start();
	}
 
	public void dispose() {
		if (universe != null) {
			universe.shutdown();
		}
 
		super.dispose();
	}
 
	public void windowActivated(WindowEvent arg0) {
	}
 
	public void windowClosed(WindowEvent arg0) {
	}
 
	public void windowClosing(WindowEvent arg0) {
		universe.shutdown();
		this.dispose();
	}
 
	public void windowDeactivated(WindowEvent arg0) {
	}
 
	public void windowDeiconified(WindowEvent arg0) {
	}
 
	public void windowIconified(WindowEvent arg0) {
	}
 
	public void windowOpened(WindowEvent arg0) {
	}
}

Storing Accelerometer Data

And what is this AccelerometerMonitor class you ask? Well, it turns out that getting the latest Accelerometer data is rather expensive. And since we want the screen to animate fairly quickly we can't afford long calls to get the data. So, rather than accessing the Accelerometer from the main simulation thread, a separate thread is executed that periodically (in this case 2 times a second) reads from the device. The main thread then accesses this cached data. AccelerometerMonitor.java:

package simapp;
 
import java.io.IOException;
 
import com.buglabs.bug.accelerometer.pub.AccelerometerSample;
import com.buglabs.bug.accelerometer.pub.IAccelerometerSampleProvider;
 
public class AccelerometerMonitor extends Thread {
 
	private final IAccelerometerSampleProvider accel;
	private volatile boolean running;
	private volatile AccelerometerSample lastSample;
	private long sleepInterval;
 
	public AccelerometerMonitor(IAccelerometerSampleProvider accel, int sleepInterval) throws IOException {
		this.accel = accel;
		this.sleepInterval = sleepInterval;
		lastSample = accel.readSample();
	}
 
	public void shutdown() {
		this.running = false;
	}
 
	public void run() {
		this.running = true;
 
		try {
			while (this.running) {
				lastSample = accel.readSample();
				Thread.sleep(sleepInterval);
			}
		} catch (IOException e) {
			e.printStackTrace();
		} catch (InterruptedException e) {				
		}
	}
 
	public AccelerometerSample getLastSample() {
		return lastSample;
	}
}

Augmenting Simulation

Now let's modify Universe.java such that in the simulation thread we add the accelerometer vector to each body:

/*
 * This class is based on the class work for Princeton class COS126.  Material found here:
 * http://www.cs.princeton.edu/courses/archive/spr01/cs126/assignments/nbody.html
 * 
 */
 
package simapp;
 
import com.buglabs.bug.accelerometer.pub.AccelerometerSample;
 
/**
 * Represents the app thread and the core keeper of state in the app.
 *
 */
public class Universe extends Thread {
	private static final double ACCEL_MULTIPLIER = 1e26;
	private final double radius; // radius of universe
	private final int N; // number of bodies
	private final Body[] orbs; // array of N bodies
	private final SimulationCanvas canvasAdapter;
	private final double dt;
	private boolean running = false;
	private final AccelerometerMonitor accelMonitor;
 
	// read universe from standard input
	public Universe(SimulationCanvas canvasAdapter, Seed seed, AccelerometerMonitor accelMonitor) {
		this.canvasAdapter = canvasAdapter;
		this.accelMonitor = accelMonitor;
		this.dt = seed.getTimeDelta();
 
		N = seed.getBodies().size();
 
		// the set scale for drawing on screen
		radius = seed.getRadius();
		canvasAdapter.setXscale(-radius, +radius);
		canvasAdapter.setYscale(-radius, +radius);
 
		// read in the N bodies
		orbs = (Body []) seed.getBodies().toArray(new Body[N]);
	}
 
	// increment time by dt units, assume forces are constant in given interval
	public void increaseTime(double dt) {
 
		// initialize the forces to zero
		Vector[] f = new Vector[N];
		for (int i = 0; i < N; i++) {
			f[i] = new Vector(new double[2]);
		}
 
		// compute the forces
		for (int i = 0; i < N; i++) {
			for (int j = 0; j < N; j++) {
				if (i != j) {
					f[i] = f[i].plus(orbs[j].forceTo(orbs[i]));
				}
			}
		}
 
		// move the bodies
		for (int i = 0; i < N; i++) {
			f[i] = f[i].plus(getAccelerometerVector());
			orbs[i].move(f[i], dt);
		}
	}
 
	private Vector getAccelerometerVector() {
		AccelerometerSample sample = accelMonitor.getLastSample();
 
		if (sample == null) {
			return new Vector(new double[] {0, 0});
		}
 
		double x = accelMonitor.getLastSample().getAccelerationX() * ACCEL_MULTIPLIER;
		double y = accelMonitor.getLastSample().getAccelerationY() * ACCEL_MULTIPLIER;
 
		return new Vector(new double[] {x, y});
	}
 
	private void renderBodies() {
		for (int i = 0; i < N; i++) {
			canvasAdapter.setPenRadius(orbs[i].getRadius());
	        canvasAdapter.point(orbs[i].getX(), orbs[i].getY());
		}
	}
 
	public void run() {
		running = true;
		try {
			while (running) {
				canvasAdapter.clear();
				this.increaseTime(dt);
				this.renderBodies();
				Thread.sleep(20);
			}
		} catch (InterruptedException e) {
		}
	}
 
	public void shutdown() {
		running = false;
	}
}

Note the new constant ACCEL_MULTIPLIER. This value is multiplied with the accelerometer data, to produce a vector of meaningful magnitude. Modifying this value will change how much influence the accelerometer has on the simulation. Also, when running this simulation on the Virtual BUG, the accelerometer data will probably have no effect. The real BUG is required to see the effects of the accelerometer.

Conclusion

After making the changes in Step 3, you should be able to run the application on the BUG. You can experiment with many different initial configurations by changing what's passed to the Universe constructor.

This tutorial covered a simple simulation app using two OSGi services provided by the BUG LCD module. In addition multiple thread were utilized to ensure that costly operations did not prevent UI updates. Some things that may be interesting to explore from this point:

  • Use the accelerometer in the Motion detector for more accurate effects.
  • Incorporate other modules, such as the VonHippel or Camera module. How could a camera image be applicable to an N-Body simulation?
  • Constrain the bodies to the visible area of the screen.
  • Implement double buffering for a smoother view.