In this tutorial we will build a simple model of particles bouncing off walls, leaving trails, and interacting with one another.
This tutorial teaches:
We'll start with a basic Model. In the sim/app/tutorial3 application directory reate a file called Tutorial3.java. Add to it:
package sim.app.tutorial3; import sim.engine.*; import sim.field.grid.*; import sim.util.*; import ec.util.*; public class Tutorial3 extends SimState { public DoubleGrid2D trails; public SparseGrid2D particles; public int gridWidth = 100; public int gridHeight = 100; public int numParticles = 500; public Tutorial3(long seed) { super(new MersenneTwisterFast(seed), new Schedule(3)); } public void start() { super.start(); trails = new DoubleGrid2D(gridWidth, gridHeight); particles = new SparseGrid2D(gridWidth, gridHeight);
Some explanation. First, note that the schedule is being initialized with a 3. This indicates the number of orders in the Schedule. Here we will have orders 0, 1, and 2. An order is a subdivision of a single time tick. If you schedule items all at time tick 407, then when 407 rolls around, all the items scheduled for 407 (order 0) will be executed first; then those scheduled for 407 (order 1) will be executed next; then finally those scheduled for 407 (order 2) will be executed. The idea is that all items scheduled for later time ticks will be executed later; but within a time given tick, the order defines which items will be executed first.
Is there a 2 Dimensional Grid of Objects like DoubleGrid2D is for doubles?
Yes. It's called ObjectGrid2D, and it works just like DoubleGrid2D and IntGrid2D. It differs from SparseGrid2D in important ways: first, ObjectGrid2D allows one, and exactly one, object per array location (it's just a wrapper for an Object[][] array). SparseGrid2D uses hash tables rather than arrays, and allows many objects to pile up on the same array location. Also, SparseGrid2D is much faster at looking up the location of an object, and is much more memory efficient, but looking up objects at locations is slower (a constant hash table lookup overhead). Generally speaking, if you need an array of Objects, use ObjectGrid2D. If you want a few agents scattered in different locations (possibly the same locations), use SparseGrid2D. |
Also, note the declaration of a SparseGrid2D and DoubleGrid2D. Previously we used an IntGrid2D, which was little more than a wrapper for a two dimensional array of integers. Likewise, a DoubleGrid2D wraps a two dimensional array of doubles.
SparseGrid2D is a different beast. It's not an array of objects: instead it uses hash tables internally, and allows you to put an Object at a given location. Multiple objects may occupy the same location in a SparseGrid2D.
In this tutorial, we're going to make a class called Particle, which is an agent that bounces around the environment. Particles will be stored in the SparseGrid2D and will leave "trails" in the DoubleGrid2D. Particles will have a direction (x and y values of 0, 1, or -1 each). Particles will be embodied agents (they're both agents -- they're Steppable and manipulate the world ; and they're also in the world). Let's make numParticles Particles, put them in the world at random locations, given them random directions, and schedule them to be fired as Steppables at every time tick. Add:
Particle p; for(int i=0 ; i < numParticles ; i++) { p = new Particle(random.nextInt(3) - 1, random.nextInt(3) - 1); // random direction schedule.scheduleRepeating(p); // Starting at Epoch, Order 0, once every time step... particles.setObjectLocation(p, new Int2D(random.nextInt(gridWidth),random.nextInt(gridHeight))); // random location }
Why not use java.awt.Point?
Point is mutable. So are java.awt.geom.Point2D, etc. But SparseGrid2D uses hashtables. Mutable objects break hashtables: put the location in the hash table as a key, then change the values in the location. Instant broken hashtable. This is a serious Java flaw that Sun has not said much about. |
Note the use of Int2D. This class is essentially the same as java.awt.Point: it holds an x and a y location as ints. However, Int2D is immutable -- once it is constructed, its values cannot be changed. SparseGrid2D stores locations as Int2D. Likewise, SparseGrid3D stores locations as Int3D, and other fields use Double2D or Double3D.
If you use the default method to schedule something or schedule it repeating (as we did the Particles), it's automatically order 0, and starting at the Epoch (the first time tick after the start of the simulation). As the agents traverse the world, they will leave trails by setting DoubleGrid2D locations to 1.0. Let's also create a "decreaser" agent which slowly decreases the DoubleGrid2D values. We'll schedule it to happen at every time tick as well, starting at the Epoch, -- but at Order 2 (so it happens after the Particles do their thing):
// Schedule the decreaser Steppable decreaser = new Steppable() { public void step(SimState state) { // decrease the trails trails.multiply(0.9);
The multiply function is a simple mapper which multiplies all values in the 2D array by 0.9. There are other simple functions like that so you don't have to do the for-loops yourself. Continuing...
} }; schedule.scheduleRepeating(Schedule.EPOCH,2,decreaser,1); }
What goes at Order 1?
Patience, grasshopper, patience. |
This full form of scheduleRepeating says: starting at the Epoch, schedule the decreaser to be repeatedly pulsed, once a timestep, at Order 2.
We finish with the standard main(), which you should be familiar with by now:
public static void main(String[] args) { Tutorial3 tutorial3 = null; for(int x=0;x<args.length-1;x++) if (args[x].equals("-checkpoint")) { SimState state = SimState.readFromCheckpoint(new java.io.File(args[x+1])); if (state == null) System.exit(1); else if (!(state instanceof Tutorial3)) { System.out.println("Checkpoint contains some other simulation: " + state); System.exit(1); } else tutorial3 = (Tutorial3)state; } if (tutorial3==null) { tutorial3 = new Tutorial3(System.currentTimeMillis()); tutorial3.start(); } long time; while((time = tutorial3.schedule.time()) < 5000) { if (time % 100 == 0) System.out.println(time); if (!tutorial3.schedule.step(tutorial3)) break; if (time%500==0 && time!=0) { String s = "tutorial3." + time + ".checkpoint"; System.out.println("Checkpointing to file: " + s); tutorial3.writeToCheckpoint(new java.io.File(s)); } } tutorial3.finish(); } }
Save the file.
Now we'll write the Particle agent. Create a file called Particle.java, and in it, add:
package sim.app.tutorial3; import sim.engine.*; import sim.util.*; public class Particle implements Steppable { public boolean randomize = false; public int xdir; // -1, 0, or 1 public int ydir; // -1, 0, or 1 public Particle(int xdir, int ydir) { this.xdir = xdir; this.ydir = ydir; }
The particle will behave as follows. If it hits a wall, it "bounces" off the wall. If it lands on another particle, all particles at that location will have their direction randomized. The randomization happens by setting their randomize flags, and next time they're stepped, the Particles will randomize their own directions if necessary. The first thing we need to do is get the current position of the Particle. We could have stored that in the Particle itself, but it's easy enough to just use the location it was set to in the SparseGrid2D:
public void step(SimState state) { Tutorial3 tut = (Tutorial3)state; Int2D location = tut.particles.getObjectLocation(this);
Now we leave a trail in the DoubleGrid2D at our location, and randomize direction if needed
// leave a trail tut.trails.field[location.x][location.y] = 1.0; // Randomize my direction if requested if (randomize) { xdir = tut.random.nextInt(3) - 1; ydir = tut.random.nextInt(3) - 1; randomize = false; }
Now we move and check to see if we hit a wall...
// move int newx = location.x + xdir; int newy = location.y + ydir; // reverse course if hitting boundary if (newx < 0) { newx++; xdir = -xdir; } else if (newx >= tut.trails.getWidth()) {newx--; xdir = -xdir; } if (newy < 0) { newy++ ; ydir = -ydir; } else if (newy >= tut.trails.getHeight()) {newy--; ydir = -ydir; }
Now store our new location in the SparseGrid2D.
// set my new location Int2D newloc = new Int2D(newx,newy); tut.particles.setObjectLocation(this,newloc);
Last, get all the objects at our new location out of the SparseGrid2D and set their randomize flags.
// randomize everyone at that location if need be Bag p = tut.particles.getObjectsAtLocation(newloc); if (p.numObjs > 1) { for(int x=0;x < p.numObjs;x++) ((Particle)(p.objs[x])).randomize = true; } } }
Bags are only for Objects?
Yes. But we also provide extensible arrays for integers (IntBag) and doubles (DoubleBag) as well. They have nearly identical functionality to Bags. |
We just used a Bag for the first time. A sim.util.Bag is a wrapper for an array, like an ArrayList or a Vector. The difference is that Bags have explicitly public, modifiable underlying arrays. Specifically, Bags have an array of Objects called objs, and a count of objects called numObjs. The objects stored in a Bag run from position 0 through numObjs-1 in the objs array (objs can be longer than is actually used to store the objects). Bags also have a special fast remove function which doesn't guarantee maintaining order. The result is that Bags are significantly (over 3 times) faster than ArrayLists or Vectors.
Save the file.
Now we'll write the Particle agent. Create a file called Tutorial3WithUI.java. We begin by adding lots of stuff you've already seen before -- except we're specifying a SparseGrid2DPortrayal to draw our SparseGrid2D, and we're specifying a FastValueGrid2DPortrayal to draw our DoubleGrid2D (various ValueGrid2DPortrayals can draw either DoubleGrid2D or IntGrid2D). To the file, add:
package sim.app.tutorial3; import sim.engine.*; import sim.display.*; import sim.portrayal.grid.*; import sim.portrayal.*; import java.awt.*; import javax.swing.*; public class Tutorial3WithUI extends GUIState { public Display2D display; public JFrame displayFrame; SparseGrid2DPortrayal particlesPortrayal = new SparseGrid2DPortrayal(); FastValueGrid2DPortrayal trailsPortrayal = new FastValueGrid2DPortrayal("Trail"); public static void main(String[] args) { Tutorial3WithUI t = new Tutorial3WithUI(); Console c = new Console(t); c.setVisible(true); } public Tutorial3WithUI() { super(new Tutorial3(System.currentTimeMillis())); } public Tutorial3WithUI(SimState state) { super(state); } public String getName() { return "Tutorial3: Particles"; } public String getInfo() { return "<H2>Tutorial3</H2><p>An odd little particle-interaction example."; } public void quit() { super.quit(); if (displayFrame!=null) displayFrame.dispose(); displayFrame = null; // let gc display = null; // let gc } public void start() { super.start(); setupPortrayals(); // this time, we'll call display.reset() and display.repaint() in setupPortrayals() } public void load(SimState state) { super.load(state); setupPortrayals(); // likewise... }
Why not from range Transparent Clear to Opaque White?
Don't do this unless you need to for your problem. FastValueGridPortrayal2D can draw grids in two ways: either by drawing lots of rectangles, or by poking a bitmap and stretching the bitmap to fit the area. You specify this with setBuffering(...) (or rely on the defaults, which are platform-dependent). The problem is that in Windows and X Windows, the first method is the fastest when your colors are all opaque, but the slowest by far if the colors are transparent -- except for 100% transparent regions, which the simulator avoids drawing at all (fast!). But the second method consumes more memory and generally requires you to increase your heap size manually, so Windows and X Windows by default use the first method. If you definitely need transparency (perhaps to overlay Portrayal2D's), call setBuffering(...) and increase your heap size as specified in the sim.portrayal.grid.FastValueGridPortrayal2D documentation. MacOS X's default is to always use the bitmap (MacOS X is handles bitmaps much more efficiently), except when writing to a movie or to a snapshot (see the documentation). On MacOS X, you'll likely never need to deviate from the defaults. |
public void setupPortrayals() { // tell the portrayals what to // portray and how to portray them trailsPortrayal.setField( ((Tutorial3)state).trails); trailsPortrayal.setLevels(0.0,1.0, Color.back,Color.white);
So far you've only seen FastValueGrid2DPortrayals. These are very basic portrayals which always draw their underlying doubles as squares of a given color. But that's not the general mechanism for the simulator. Instead, various Field Portrayals usually draw their underlying objects by looking up the proper SimplePortrayal registered for that object, and asking it to draw the object on-screen. The procedure for looking up the proper SimplePortrayal is used is as follows:
What other SimplePortrayals are there?
SquarePortrayal2D, HexaPortrayal2D, and ImagePortrayal2D come to mind. It's easy to make your own Portrayals as well. |
For now, we use the setPortrayalForAll method to specify that all objects stored in the SparseGrid2D should be portrayed with the same Simple Portrayal. The SimplePortrayal we pick is a green OvalPortrayal2D.
particlesPortrayal.setField(((Tutorial3)state).particles); particlesPortrayal.setPortrayalForAll( new sim.portrayal.simple.OvalPortrayal2D(Color.green) ); // reschedule the displayer display.reset(); // redraw the display display.repaint(); }
Last, we need to build the display and add the field portrayals to it. First, add stuff you've seen before:
public void init(Controller c) { super.init(c); display = new Display2D(400,400,this,1); displayFrame = display.createFrame(); c.registerFrame(displayFrame); displayFrame.setVisible(true); display.setBackdrop(Color.black);
Now we attach the field portrayals. The display will draw the portrayals on top of each other. The order of drawing is the same as the order of attachment. We want the trails to be drawn first, then the particles drawn on top of them. Display2D has a menu option that lets you selectively turn on or off the drawing of these objects -- the names of the menus should be "Trails" and "Particles". Thus we write:
display.attach(trailsPortrayal,"Trails"); display.attach(particlesPortrayal,"Particles"); } }
Save the file. Compile all three java files. Run the java code as java sim.app.tutorial3.Tutorial3WithUI. You should be able to see agents bouncing around and leaving trails! The Layers menu (the icon at the top-left corner of the Display2D) lets you turn on or off either of the two layers.
The simulator relies on Java's Serialization facility to do its checkpointing. Serialization relies on "version" numbers, essentially unique IDs for each class. They don't have to be correct; they just ought to be (but don't need to be) different for different versions of a class.
But there's a gotcha.
Will Sun ever fix this stupdity?
Likely not. This misfeature has been on Sun's bugfix request list for years now. |
This only matters if you care about moving a checkpoint file from one platform to another -- for example, running it in Linux and visualizing it on the Mac. Of you don't plan on ever doing this, you can skip the rest of this section.
What we need is hard-coded version numbers for any non-static inner class and for its enclosing classes. You can make up any version number you want. But if you'd like to be extra-official today, the "one true" way to do it is to call the serialver program, part of the Java SDK. Pass it the full class name. non-static inner classes should be provided with the dollar-sign stuff appended to the end. For example, on MacOS X, compiling the Tutorial3 file results in the classes "sim.app.tutorial3.Tutorial3" and "sim.app.tutorial3.Tutorial3$1". We pass those to serialver. Note the use of the backslash in Unix to allow the $ to get passed through:
poisson> serialver sim.app.tutorial3.Tutorial3 sim.app.tutorial3.Tutorial3: static final long serialVersionUID = 9115981605874680023L; poisson> serialver sim.app.tutorial3.Tutorial3\$1 sim.app.tutorial3.Tutorial3$1: static final long serialVersionUID = 6330208160095250478L;
Now that we have some version numbers (or now that we've made some up! that's fine too), we can put them into the Tutorial3 code. Tutorial3$1 is the Decreaser. In the Tutorial3 file:
FROM... | CHANGE TO |
// Schedule the decreaser Steppable decreaser = new Steppable() { public void step(SimState state) { // decrease the trails trails.multiply(0.9); } }; | // Schedule the decreaser Steppable decreaser = new Steppable() { public void step(SimState state) { // decrease the trails trails.multiply(0.9); } static final long serialVersionUID = 6330208160095250478L; }; |
Similarly, we'll set the version number for Tutorial3:
FROM... | CHANGE TO |
tutorial3.finish(); } } | tutorial3.finish(); } static final long serialVersionUID = 9115981605874680023L; } |
Now things should checkpoint and restore on different platforms smoothly. But there's a further gotcha. Because we've hard-coded the serialVersionUIDs, if we change the classes in any way, they'll be incompatible with the serialized versions of previous classes but Java won't know that and icky things could happen. This happens all through the simulator, so: if you change and recompile your code, you should not then try to load an old checkpoint file created using the old code.