In this tuiorial we will visualize the model created in Tutorial1.
This tutorial teaches:
In the sim/app/tutorial1and2 directory, create a file called Tutorial2.java In this file, add:
package sim.app.tutorial1and2; import sim.engine.*; import sim.display.*; import sim.portrayal.grid.*; import java.awt.*; import javax.swing.*; public class Tutorial2 extends GUIState { public Tutorial2() { super(new Tutorial1(System.currentTimeMillis())); } public Tutorial2(SimState state) { super(state); }
Why no serialVerUID?
This isn't a model object. It's not serializable. So no need. |
This simulation library makes a very bright dividing line between the simulation model and visualizers of that model. This enables us (as we'll discover later) to run the model without visualization, then hook visualizers to it and see how it's doing (on a different machine no less), then unhook them and let it continue running at that point.
To do this, we need to make certain that no visualization objects are mixed into the simulation. We do this by wrapping the SimState in an external object, called a sim.display.GUIState, which is the sole access point for external visualization tools. The GUIState is constructed by passing in the SimState it's supposed to wrap. In this case, that's a Tutorial1 object.
Additionally, the GUIState provides a name for the simulation, and a description of the simulation in HTML. Add:
public static String getName() { return "Tutorial 2: Life"; } public static Object getInfo() { return "<H2>Conway's Game of Life</H2>" + "<p>... with a B-Heptomino"; }
I'd prefer to load my description from an HTML file.
No problem. If you don't override the getInfo() method at all, the default version loads from a file called index.html which must be located in the same directory as your class file (in this case, Tutorial2.class) You can also specify a URL for an HTML file of your choosing: instead of returning a String in getInfo(), return a java.net.URL |
The GUIState is to the visualized simulation as the SimState is to the underlying model: it's essentially a singleton that holds everything we care about. To the GUIState, we'll add one visualization tool: a sim.display.Display2D, which is a Swing JComponent that lets us examine 2D model information. The Display2D isn't in a JFrame window by default, so we need to put it in one (in fact it has a convenience function which "sprouts" the window for us). Add:
public Display2D display; public JFrame displayFrame;
A sim.portrayal.Portrayal is an object which knows how to draw an object and/or allow the user to manipulate it graphically. There are 2D portrayals for fields (various subclasses of sim.portrayal.FieldPortrayal2D) and for the objects or values stored inside fields (various subclasses of sim.portrayal.SimplePortrayal2D). Most field portrayals work by using their underlying fields to ascertain the objects that need to be drawn, then requesting simple portrayals for those objects, and telling the simple portrayals to draw them.
The portrayal we will be concerned with knows how to draw IntGrid2D (and DoubleGrid2D) fields. There are two versions of this portrayal: the flexible sim.portrayal.grid.ValueGridPortrayal2D and the faster but inflexible sim.portrayal.grid.FastValueGridPortrayal2D. The first version allows the user to specify a custom simple portrayal to draw each value in the grid. The second version can only draw its values as colored squares no matter what. We'll use the second one. Add:
FastValueGridPortrayal2D gridPortrayal = new FastValueGridPortrayal2D();
We begin by writing our own private method which sets up the gridPortrayal:
public void setupPortrayals() { // tell the portrayals what to portray and how to portray them gridPortrayal.setField(((Tutorial1)state).grid); gridPortrayal.setMap( new sim.util.gui.SimpleColorMap( new Color[] {new Color(0,0,0,0), Color.blue})); }
The first line attaches the FastValueGridPortrayal2D to its underlying field (the grid). This isn't done in the FastValueGridPortrayal2D's constructor because at that point the grid may not exist yet.
How about gradients?
SimpleColorMap also supports linear gradients. Calling new SimpleColorMap(minValue,maxValue,minColor,maxColor) lets you state that colors should run smoothly from minColor to maxColor for the value range from minValue to maxValue. You can also make SimpleColorMaps which use both gradients and color lookup tables (the color table overrides the gradient within the color table's range). For more sophisticated drawing, you can provide your own underlying SimplePortrayal2D. |
Like SimState, GUIState has start() and finish() methods, which in turn call the underlying SimState's start() and finish() methods. We'll override just the start method:
public void start() { super.start(); setupPortrayals(); // set up our portrayals display.reset(); // reschedule the displayer display.repaint(); // redraw the display }
The start() method is where we prepare the visualizer for the start of a model run. To do this, we need to attach the portrayals to the model, reset the display, and repaint the display once.
The GUIstate provides a "schedule wrapper" for Steppable objects which need to schedule themselves to assist in visualization, but are not part of the model (keep in mind we don't want to schedule anything in the SimState's schedule that's not part of the underlying model). The Display2D is one of these objects: it needs to update its display after each time step. It schedules itself in this "schedule wrapper" this by calling the GUIState's scheduleImmediateRepeat(...) method. This happens in the display's reset() method, which is why it must be called here at the start of a model run.
A GUIState also needs to know when the GUI application has been launched and when it is being quit. This can't be inside the start() and finish() methods because a simulation can be started and restarted many times while the application is running. Thus this information is supplied by the init(Controller) and quit() methods. We won't bother with a quit() method, but we need the init(Controller) method
The init() method is passed a sim.display.Controller object. A Controller is responsible for running the simulation. The Controller calls the start() and finish() methods, and calls the GUIState's step() method (which in turn calls the underlying model's step() method). You can do various things with the Controller (and especially with its most common subclass, sim.display.Console); but for our purposes there's one useful function: registering our Display2D's JFrame in the Controller's graphical list of JFrames. This has several benefits. First, it allows us to "hide" the JFrame by closing it, and "unhide" it by picking it graphically in the Controller. Second, whenever the user modifies the model graphically, the JFrame will be repainted to give the Display2D a chance to update itself to reflect this modification. Without registry, the Display2D will not repaint itself automatically until the next time step. Third, all windows in the registry will get dispose() called on them automatically before the program quits. Register your windows. It's a Good Thing.
Add:
public void init(Controller c) { super.init(c); // Make the Display2D. We'll have it display stuff later. Tutorial1 tut = (Tutorial1)state; display = new Display2D(tut.gridWidth * 4, tut.gridHeight * 4,this); displayFrame = display.createFrame(); c.registerFrame(displayFrame); // register the frame so it appears in the "Display" list displayFrame.setVisible(true); display.attach(gridPortrayal,"Life"); // attach the portrayals // specify the backdrop color -- what gets painted behind the displays display.setBackdrop(Color.black); }
We first created a Display2D with a drawing area of 400x400. Since we have a 100x100 grid, this makes our cells 4x4 pixels each. Second, we let the Display2D sprout its own frame by calling createFrame(). Third, we registered the frame, then made the window visible.
Next, we attached our grid to the display. You can attach multiple FieldPortrayals to a display, and they will be drawn one on top of the other (hence why transparency is nice). Each FieldPortrayal is attached with a simple name which appears in a menu on the Display2D window.
Last, we set the "backdrop color" of the Display2D. This sets the color to be painted behind all of the Portrayals.
What about Inspectors?
Portrayals also provide inspectors (part of what Swarm might call "probes") that give the user a chance to read and manipulate information about objects. If you double-click on any square on the grid, the inspector for that square pops up showing the location of the square and its current value. Note that you cannot change the value to things other than 0 or 1. This is because we only gave colors (transparent and blue) for these values when we called setColorTable(...). You can make custom inspectors in a variety of ways; we'll discuss this in a later tutorial. |
public static void main(String[] args) { Tutorial2 tutorial2 = new Tutorial2(); Console c = new Console(tutorial2); c.setVisible(true); }
However, it's better style to instead just call the createController class, which by default does the same thing as those last two lines:
public static void main(String[] args) { new Tutorial2().createController(); }
If you wanted a controller other than a Console, you just override createController() to create and start it.
Why do it this way? Besides having fewer lines, this also makes it easy for your simulation to be created in other ways than the main() method -- for example by the user choosing "New Simulation" in the Console menu -- because the calling method doesn't need to know whether to create a console or some other controller: your class makes the controller for it.
Anyway, this creates a Tutorial2. It then creates a sim.display.Console, which is a Controller that provides a nice graphical interface (the Console is the window with the Play/Stop/Pause buttons). The Console needs to know what GUIState to launch and run -- we pass it our Tutorial2. Then we make the Console window visible.
Now we're ready to test. Save the Tutorial2.java file. Compile the CA.java, Tutorial1.java, and Tutorial2.java files. Then run the program as java sim.app.tutorial1and2.Tutorial2
The program will launch, displaying the grid cells, with the live ones hilighted in blue. Press play and watch it go.
One nice point about the system is its ability to checkpoint out the model, and to read models from checkpoints. This permits us to do the following sequence, for example:
For some experiments this isn't really a necessary feature. But the simulation system was designed for large numbers of long, complex runs on back-end machines, with occasional visualization of the results.
Checkpoint files can even be traded among different operating systems. This trick is done by using Java's Serialization features. To do checkpointing, all classes used in the underlying model must implement the interface java.io.Serializable. Since CA implements Steppable, it automatically implements Serializable as well.
To checkpoint out from Tutorial1, we need to add some features to the main() method. In the Tutorial1.java file, delete the existing main() method. In its place, write:
public static void main(String[] args) { Tutorial1 tutorial1 = null; // should we load from checkpoint? for(int x=0;x<args.length-1;x++) // "-fromcheckpoint" can't be the last string if (args[x].equals("-fromcheckpoint")) { SimState state = SimState.readFromCheckpoint(new java.io.File(args[x+1])); if (state == null) // there was an error -- quit (error will be displayed) System.exit(1); else if (!(state instanceof Tutorial1)) // uh oh, wrong simulation stored in the file! { System.out.println("Checkpoint contains some other simulation: " + state); System.exit(1); } else // we're ready to lock and load! tutorial1 = (Tutorial1)state; }
This little chunk of code lets us start a Tutorial1 simulation on the command line, loading from an existing checkpoint file using the -fromcheckpoint parameter. Continuing:
// ...or should we start fresh? if (tutorial1==null) // no checkpoint file requested { tutorial1 = new Tutorial1(System.currentTimeMillis()); tutorial1.start(); }
Here, if no -fromcheckpoint argument was provided, we just start a brand-spanking-new Tutorial1. Continuing, we run the main loop as before, but every 500 steps we write out a checkpoint file of the model.
long steps; do { if (!tutorial1.schedule.step(tutorial1)) break; steps = tutorial1.schedule.getSteps(); if (steps % 500 == 0) { System.out.println("Steps: " + steps + " Time: " + tutorial1.schedule.getTime()); String s = steps + ".Tutorial1.checkpoint"; System.out.println("Checkpointing to file: " + s); tutorial1.writeToCheckpoint(new java.io.File(s)); } } while(steps < 5000); tutorial1.finish(); System.exit(0); // make sure any threads finish up }
Now, we need to add an additional function to the Tutorial2.java file: the load() method. This method is similar to start(), except that it is called after a checkpoint has been loaded into the visualizer.
Just like in start(), in load(), we typically have to attach the visulization equipment to the newly-loaded model. Add the following to the Tutorial2.java file:
public void load(SimState state) { super.load(state); setupPortrayals(); // set up our portrayals for the new SimState model display.reset(); // reschedule the displayer display.repaint(); // redraw the display }
Save and compile the Tutorial1.java and Tutorial2.java files. Then run java sim.app.tutorial1and2.Tutorial1 and note that it writes out a checkpoint file every 500 timesteps. We only care about the first one, so feel free to quit the program after that checkpoint has been written out.
Next, let's view the model at that checkpoint. Run java sim.app.tutorial1and2.Tutorial2 and click on the Console window (the window with the Play/Pause/Stop buttons). Choose Open... from the File menu. Select the file 500.tutorial1.checkpoint. The display will change to reflect timestep 499, and the Console will go into paused mode. Unpausing the simulation results in the model running starting at timestep 500.
Let it run for a little while. Then select Save As... from the File menu. Save out the simulation with the file name new.checkpoint then quit the program.
Last, let's start up the model from the command line starting at the timestep where we saved out new.checkpoint. Simply run java sim.app.tutorial1and2.Tutorial1 -fromcheckpoint new.checkpoint and watch it go!
For extra fun, try trading the checkpoint file across different operating systems (MacOS X and Linux for example). This mostly works if the systems are running the same Java version (say, 1.4.1). There's another gotcha here regarding inner classes which we'll get to in Tutorial 3.
This main(...) loop got complicated. But there are still more command-line arguments we might like: like quitting after N steps, or specifying how often to write out the checkpoint, or providing the random number generator seed.
To handle most of the common situations, we've whipped up a somewhat more complicated main(...) loop for you, called SimState.doLoop(...). In the Tutorial1.java file, replace the main() with a whole new one that looks like this:
public static void main(String[] args) { doLoop(Tutorial1.class, args); System.exit(0); }
Well! That's a lot easier. doLoop(...) needs two things: the class that it should instantiate a simulation from, and the command line arguments. That's it. Compile the Tutorial1.java file. We start by running java sim.app.tutorial1and2.Tutorial1 -help and see the following message:
Format: java sim.app.tutorial1and2.Tutorial1 \ [-help] [-checkpoint C] [-repeat R] [-seed S] \ [-for F] [-until U] [-time T] [-docheckpoint D] -help Shows this message. -repeat R Long value > 0: Runs the job R times. The random seed for each job is the provided -seed plus the job# (starting at 0). Default: runs once only: job number is 0. -checkpoint C String: loads the simulation from file C for job# 0. Further jobs are started new using -seed as normal. Default: starts a new simulation rather than loading one. -until U Double value >= 0: the simulation must stop when the simulation time U has been reached or exceeded. Default: don't stop. -for N Long value >= 0: the simulation must stop when N simulation steps have transpired. Default: don't stop. -seed S Long value not 0: the random number generator seed. Default: the system time in milliseconds. -time T Long value >= 0: print a timestamp every T simulation steps. If 0, nothing is printed. Default: auto-chooses number of steps based on how many appear to fit in one second of wall clock time. Rounds to one of 1, 2, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, etc. -docheckpoint D Long value > 0: checkpoint every D simulation steps. Default: never. Checkpoints files named <steps>.<job#>.Tutorial1.checkpoint
Let's try some. We'll run 3 times for 1000 steps each, checkpointing every 500 steps, with a random seed of 2000 (then 2001, then 2002). Here we go: java sim.app.tutorial1and2.Tutorial1 -repeat 3 -docheckpoint 500 -for 1000 -seed 2000 We get printed out something like this:
Job: 0 Seed: 2000 Starting sim.app.tutorial1and2.Tutorial1 Steps: 250 Time: 249 Rate: 131.92612 Steps: 500 Time: 499 Rate: 130.0052 Checkpointing to file: 500.0.Tutorial1.checkpoint Steps: 750 Time: 749 Rate: 117.92453 Steps: 1000 Time: 999 Rate: 134.77089 Checkpointing to file: 1000.0.Tutorial1.checkpoint Quit Job: 1 Seed: 2001 Starting sim.app.tutorial1and2.Tutorial1 Steps: 250 Time: 249 Rate: 130.89005 Steps: 500 Time: 499 Rate: 136.91128 Checkpointing to file: 500.1.Tutorial1.checkpoint Steps: 750 Time: 749 Rate: 129.19897 Steps: 1000 Time: 999 Rate: 135.94345 Checkpointing to file: 1000.1.Tutorial1.checkpoint Quit Job: 2 Seed: 2002 Starting sim.app.tutorial1and2.Tutorial1 Steps: 250 Time: 249 Rate: 134.26423 Steps: 500 Time: 499 Rate: 130.68479 Checkpointing to file: 500.2.Tutorial1.checkpoint Steps: 750 Time: 749 Rate: 129.06557 Steps: 1000 Time: 999 Rate: 136.61202 Checkpointing to file: 1000.2.Tutorial1.checkpoint QuitYour printout may vary depending on how many steps your computer can process in a second.
Some explanation. "Job" is the job number (starting at 0). "Seed" is the random number seed (2000, 2001, 2002) of that job. If a job is started new rather than loaded from checkpoint, you'll see "Starting..." (rather than "Loading...") Every so many steps a timestep is printed out. Unless it's hard-set in the options, it's computed to be roughly every 1 second of clock time. The timestep states "Steps": how many steps have transpired, "Time", the time of the last tep, and "Rate", the approximate number of steps per second. As you can see, checkpoints are done every 200 steps. Finally the job will state either "Quit" or "Expired". "Quit" means that the job was run for so long and then stopped. "Expired" means the job quit because there was nothing left on the Schedule to execute.
Keep in mind that the doLoop(...) facility is just a convenience function to make it easy for you to write a reasonably complex main() loop. But we've taken pains to build other main() loops for you so you can see that all you have to do to run a MASON simulation is create a SimState, start() it, step() its schedule some number of times, then finish() it and throw it away. It's self-contained: don't be afraid of it.