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); }
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 String getName() { return "Conway's Game of Life"; } public String getInfo() { return "<H2>Conway's Game of Life</H2>" + "<p>... with a B-Heptomino"; }
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.ValueGrid2DPortrayal and the faster but inflexible sim.portrayal.grid.FastValueGrid2DPortrayal. 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:
FastValueGrid2DPortrayal gridPortrayal = new FastValueGrid2DPortrayal();
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.setColorTable(new Color[] {new Color(0,0,0,0), Color.blue}); }
The first line attaches the FastValueGrid2DPortrayal to its underlying field (the grid). This isn't done in the FastValueGrid2DPortrayal's constructor because at that point the grid may not exist yet.
How about gradients?
FastValueGrid2DPortrayal and ValueGrid2DPortrayal also support linear gradients. The setLevels(minValue,maxValue,minColor,maxColor) method lets you state that colors should run smoothly from minColor to maxColor for the value range from minValue to maxValue. This can be used in conjunction with color tables as well (the color table overrides setLevels in the color table range). For more sophisticated drawing, you 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 tick. 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,1); 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); }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. Th
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++) // "-checkpoint" can't be the last string if (args[x].equals("-checkpoint")) { 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 -checkpoint parameter. Continuing:
// ...or should we start fresh? if (tutorial1==null) // no checkpoint file requested { tutorial1 = new Tutorial1(System.currentTimeMillis()); tutorial1.start(); }
Here, if no -checkpoint 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 time; while((time = tutorial1.schedule.time()) < 5000) { if (time % 500 == 0) System.out.println(time); if (!tutorial1.schedule.step(tutorial1)) break; // checkpoint if (time%500==0 && time!=0) { String s = "tutorial1." + time + ".checkpoint"; System.out.println("Checkpointing to file: " + s); tutorial1.writeToCheckpoint(new java.io.File(s)); } } tutorial1.finish(); }
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 tutorial1.500.checkpoint. The display will change to reflect timestep 500, 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 -checkpoint new.checkpoint and watch it go!
For extra fun, try trading the checkpoint file across different operating systems (MacOS X and Linux for example).