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 { private static final long serialVersionUID = 1; public DoubleGrid2D trails; public SparseGrid2D particles; public int gridWidth = 100; public int gridHeight = 100; public int numParticles = 500; public Tutorial3(long seed) { super(seed); } public void start() { super.start(); trails = new DoubleGrid2D(gridWidth, gridHeight); particles = new SparseGrid2D(gridWidth, gridHeight);
Wherever did you get such a phenominal implementation of Mersenne Twister?
Thank you! This implementation is believed to be the fastest Java version available anywhere, and is used fairly widely. And its author — who also wrote this tutorial — is understandably proud of it. :-) And yes, there's a synchronized java.util.Random subclass version available, though not in the MASON distribution... |
The MersenneTwisterFast instance we created is stored in the instance variable random. Don't worry about having to learn a new class: MersenneTwisterFast has an identical API to java.util.Random. Just call it like you've always done (for example, random.nextInt(5); to get a random integer from 0 to 4 inclusive). But keep in mind that MersenneTwisterFast isn't threadsafe.
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 step. 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); 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.
I'm not used to code like this. Can you really make a Steppable class like that?
Sure you can. It's called an anonymous class, and it's a construct for creating one-shot classes which exist solely to provide an instance of some superclass or interface. Without it, you'd need to write something like: package sim.app.tutorial3; import sim.field.grid.*; import sim.engine.*; public class MyDecreaser implements Steppable { private static final long serialVersionUID = 1; DoubleGrid2D myTrails; public MyDecreaser(DoubleGrid2D tr) { myTrails = tr; } public void step(SimState state) { trails.multiply(0.9); } }And then say... Steppable decreaser = new MyDecreaser(trails); schedule.scheduleRepeating(Schedule.EPOCH,2,decreaser,1); That's an awful lot of typing just for scheduling a Steppable which calls a single function! Instead, an anonymous class makes for simpler code. Plus note that inside the anonymous class we can directly access the trails variable, so we don't have to make some complicated constructor nonsense like in the example above. This is the approximate equivalent to what's known, in modern languages, as a closure. Note though that in Java we can only reference trails like this if it's an outer instance variable or is a final local variable. And we have some versioning issues in using anonymous classes, which may or may not mattter to you, as is discussed further below. |
// Schedule the decreaser Steppable decreaser = new Steppable() { private static final long serialVersionUID = 1; 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); }
Notice that we're scheduling the decreaser in a different fashion than before. This time we're providing four pieces of information:
An ordering is a sub-ordering of a timestep. Let's say you have ten items you want to occur at timestep 10.4321121. But you want five of those items to be stepped before the other five. You can do this by scheduling the first five for one ordering, and the second five for a higher ordering. The default ordering is 0. Within an ordering, the order of agents' firing is random.
What's the point of orderings? A variety of items occur every time the schedule steps a single timestep. For example, the GUI's displays are updated each timestep, as are various inspectors. If you want items to be performed in a certain order within a timestep, an ordering is the easy way to do it.
You could achieve a similar effect using the classes sim.engine.Sequence and sim.engine.RandomSequence, which we'll get to in Tutorial 5.
What goes at Order 1?
Patience, grasshopper, patience. |
We finish with main() introduced at the end of tutorial 2. Note the change in class.
public static void main(String[] args) { doLoop(Tutorial3.class, args); System.exit(0); } }
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 { private static final long serialVersionUID = 1; 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 SparseGridPortrayal2D to draw our SparseGrid2D, and we're specifying a FastValueGridPortrayal2D to draw our DoubleGrid2D (various ValueGridPortrayal2Ds 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; SparseGridPortrayal2D particlesPortrayal = new SparseGridPortrayal2D(); FastValueGridPortrayal2D trailsPortrayal = new FastValueGridPortrayal2D("Trail"); public static void main(String[] args) { new Tutorial3WithUI().createController(); } public Tutorial3WithUI() { super(new Tutorial3(System.currentTimeMillis())); } public Tutorial3WithUI(SimState state) { super(state); } public static String getName() { return "Tutorial3: Particles"; } public static Object 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. 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 only runs well if you increase your heap size to something bigger than normal (see the sim.portrayal.grid.FastValueGridPortrayal2D documentation), so Windows and X Windows by default use the first method. 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. You can specify which method to use by calling setBuffering(...); but it's not really necessary as the user can stipulate the method to use in the 2D Options panel as well. |
public void setupPortrayals() { // tell the portrayals what to // portray and how to portray them trailsPortrayal.setField( ((Tutorial3)state).trails); trailsPortrayal.setMap( new sim.util.gui.SimpleColorMap( 0.0,1.0,Color.black,Color.white));
So far you've only seen FastValueGridPortrayal2Ds. 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?
RectanglePortrayal2D, HexagonalPortrayal2D, 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); 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 writes objects, and all objects they point to (and so on) out to a stream automatically if you obey a few rules. If you don't obey the rules, you may get an exception such as a NotSerializableException.
Sometimes when checkpointing you might get a ClassNotFoundException perhaps. There's one last possibility: the compilers are naming your inner classes differently. This isn't very common, but it's a definite possibility, and unfortunately there's no way around it. If this occurs, your only recourse is to use the same compiler and just move your .class files over to the other machine.