Tutorial 4: Inspect a Multiagent Simulation

In this tutorial we will continue what was done in Tutorial 3, adding more features.

This tutorial teaches:

Convert Tutorial3

We begin by converting the Tutorial3 code to Tutorial4. Create a directory called sim/app/tutorial4. Copy all the Tutorial3 java files into this directory. Rename the sim/app/tutorial4/Tutorial3.java and sim/app/tutorial4/Tutorial3WithUI.java to Tutorial4.java and Tutorial4WithUI.java respectively. Then modify the three java file copies as follows:

  1. Replace all references to tutorial3 with tutorial4
  2. Replace all references to Tutorial3 with Tutorial4

At this point we'll be working solely with these copied and modified files.

Add another Display2D

Let's add another Display2D. In the Tutorial4WithUI.java, change:

FROM...CHANGE TO

public Display2D display;
public JFrame displayFrame;

public Display2D display;
public JFrame displayFrame;


public Display2D display2;
public JFrame displayFrame2;

Now we'll hook it up to only show the agents (the original display will show both the agents and the trails). Also for good measure, we'll make its background blue. And stretch it weirdly, how about 400x600? Heck, why not? Change:

FROM...CHANGE TO

display = new Display2D(400,400,this,1);
displayFrame = display.createFrame();
c.registerFrame(displayFrame);
displayFrame.setVisible(true);
display.setBackdrop(Color.black);
display.attach(trailsPortrayal,"Trails");
display.attach(particlesPortrayal,"Particles");

display = new Display2D(400,400,this,1);
displayFrame = display.createFrame();
c.registerFrame(displayFrame);
displayFrame.setVisible(true);
display.setBackdrop(Color.black);
display.attach(trailsPortrayal,"Trails");
display.attach(particlesPortrayal,"Particles");


display2 = new Display2D(400,600,this,1);
displayFrame2 = display2.createFrame();
displayFrame2.setTitle("The Other Display");
c.registerFrame(displayFrame2);
displayFrame2.setVisible(true);
display2.setBackdrop(Color.blue);
display2.attach(particlesPortrayal,"Squished Particles!");

Last, we need to update this new Display2D. Change:

FROM...CHANGE TO

// reschedule the displayer
display.reset();
        
// redraw the display
display.repaint();

// reschedule the displayer
display.reset();
display2.reset();

// redraw the display
display.repaint();
display2.repaint();

Save the file and compile all three files. Run java sim.app.tutorial4.Tutorial4WithUI and note that two Display2D windows pop up now. Try it out.

On many Java platforms, when the "Other Display" is visible (if you close it, you can get it back under the Displays tab), things slow down a lot. This is because there was hardware acceleration for circles, but "The Other Display" has to draw circles (they're squished), and there's no hardware acceleration for that. This slows stuff down a ton.

Let's make things faster by changing how the "Other Display"'s portrayal portrays the agents -- we'll have it display them as rectangles, not circles; this is hardware accelerated, and it teaches an important notion about portrayals as well. We'll add a second portrayal for the same field:

FROM...

SparseGrid2DPortrayal particlesPortrayal = new SparseGrid2DPortrayal();
CHANGE TO

SparseGrid2DPortrayal particlesPortrayal = new SparseGrid2DPortrayal();
SparseGrid2DPortrayal particlesPortrayal2 = new SparseGrid2DPortrayal();

Now we'll hook it up to the SparseGrid2D just like the other portrayal was hooked up. Only we'll tell it to use Squares rather than Ovals to draw the agents.

FROM...

particlesPortrayal.setField(((Tutorial4)state).particles);
particlesPortrayal.setPortrayalForAll( new sim.portrayal.simple.OvalPortrayal2D(Color.green) );
CHANGE TO

particlesPortrayal.setField(((Tutorial4)state).particles);
particlesPortrayal.setPortrayalForAll( new sim.portrayal.simple.OvalPortrayal2D(Color.green) );
particlesPortrayal2.setField(((Tutorial4)state).particles);
particlesPortrayal2.setPortrayalForAll( new sim.portrayal.simple.SquarePortrayal2D(Color.green) );

Last we need to tell the "Other Display" to use this different portrayal to portray the field.

FROM...

display2.attach(particlesPortrayal,"Squished Particles!");
CHANGE TO

display2.attach(particlesPortrayal2,"Squished Particles as Squares!");

Compile and run it. Now we have two fast portrayals in two displays, one drawing with ovals and one drawing with squares. The point of this exercise is to show that Displays are separate from Portrayals and Portrayals are separate from Fields or their Objects. Just as you can have multiple portrayals per display, you can have multiple displays per portrayal. Similarly, you can have multiple portrayals portraying the same field, in different ways, using different underlying SimplePortrayals.

Add Another Agent

Let's add a special kind of Particle called a BigParticle. It bounces off walls like the others; but it doesn't ever get randomized. Create the file BigParticle.java. In this file, add:

package sim.app.tutorial4;
import sim.engine.*;
import sim.util.*;

/** A bouncing particle that cannot be randomized */

public class BigParticle extends Particle
    {
    public BigParticle(int xdir, int ydir) { super(xdir,ydir); }

    public void step(SimState state)
        {
        // hard-code me to be non-randomized
        randomize = false;
        super.step(state);
        }
    }

Ah, so that's what gets scheduled in Order 1

Yes.

Save the file. Now let's schedule the BigParticle in some interesting way: it gets fired after the other Particles, but before the Decreaser, and it gets fired only once every 5 time steps. In the Tutorial4.java file, change:

FROM...

// Schedule the decreaser
Steppable decreaser = new Steppable()
CHANGE TO


BigParticle b = new BigParticle(random.nextInt(3) - 1, random.nextInt(3) - 1);
particles.setObjectLocation(b,
    new Int2D(random.nextInt(gridWidth),random.nextInt(gridHeight)));
schedule.scheduleRepeating(Schedule.EPOCH,1,b,5);

// Schedule the decreaser
Steppable decreaser = new Steppable()

What other Portrayal options are there?

We could have setPortrayalForObject(...) to give a unique portrayal for a specific agent (rather than a class). Or we could have let the agent subclass from, say, OvalPortrayal2D (or override some other SimplePortrayal) and it'd draw itself onscreen. There are more options yet.

We'll have the BigParticle only show up in the regular Display2D (not the "Other Display"). But let's have it show up as a red square slightly too big. The problem is, we've told the SparseGrid2DPortrayal that all objects should be drawn as ovals. We need to change that. Remember that Field Portrayals go through a lookup process to determine which SimplePortrayal to use for a given object. We'll just specify different objects. To do this, we tell the FieldPortrayal to use ovals for members of the Particle class, but to use slightly-too-big red squares for members of the BigParticle class. In the Tutorial4WithUI.java file, change:

FROM...

particlesPortrayal.setPortrayalForAll( new sim.portrayal.simple.OvalPortrayal2D(Color.green) );
CHANGE TO


particlesPortrayal.setPortrayalForClass(
    Particle.class, new sim.portrayal.simple.OvalPortrayal2D(Color.green) );
particlesPortrayal.setPortrayalForClass(
    BigParticle.class, new sim.portrayal.simple.SquarePortrayal2D(Color.red, 1.5) );

Sometimes the Red Square doesn't move!

Because, by random chance, its velocity is x=0, y=0. Stop and play again.

Save all the files, compile them, and run. Notice the red square slowly bouncing around oblivious to the others (but affecting them!).

Add an Inspector

In Tutorial2, we discussed Inspectors (part of what Swarm would call "probes"). These are panels which let you view and modify state information about individual objects.

SimplePortrayals provide inspectors for the objects they're portraying by implementing the getInspector method, which looks like this:


    public Inspector getInspector(LocationWrapper wrapper, GUIState state)
        {
        ...
        }

What's a Java Bean Property?

A standard way to write method names to access or modify a variable. "Foo" is a Read-Only property if there exists a method called getFoo(). "Foo" is a Read-Write property if there also exists a method called setFoo(val).

The LocationWrapper is an object which can be queried to provide the object and its current position (and it stays up-to-date automatically as well). The GUIState is the one you had created. A little while later we'll override this method to provide a custom Inspector. But for now, we'll rely on the default implementation which all SimplePortrayals provide: the SimpleInspector, which simply lets you access any Java Bean Properties.

To the Particle class, let's add some Properties. We'll add read-only properites for directions, and a read-write property for randomization (so you can randomize a Particle at any time). Add to the Particle.java file:

FROM...

public boolean randomize = false;
public int xdir;  // -1, 0, or 1
public int ydir;  // -1, 0, or 1
CHANGE TO

public boolean randomize = false;
public int xdir;  // -1, 0, or 1
public int ydir;  // -1, 0, or 1


public int getXDir() { return xdir; }
public int getYDir() { return ydir; }
public boolean getRandomize() { return randomize; }
public void setRandomize(boolean val) { randomize = val; }

Save and compile all files, and run again. Notice that if you play the simulation, then pause it, you can double-click on an agent and the Console will switch to show Inspectors for that agent (and also for the Trail location below it). Choose the agent's inspector, and note that you can now see read-only properties for XDir and YDir, and a modifiable property for Randomize.

Give BigParticle an Inspector Proxy

We have a problem. If you double-click on the BigParticle, you'll notice that its inspector also lets you modify Randomize. But BigParticle isn't supposed to be Randomizable! It's inheriting the setRandomize() and getRandomize() methods from Particle. This leads us to our first opportunity to customize an Inspector: by providing an Inspector Proxy.

Can the Inspector Proxy be an Anonymous Class?

Usually not. Indeed, all inspectable objects should be public, non-anonymous classes. They can be inner classes as long as they obey this rule. This is mostly due to quirks in Java's reflection library.

An Inspector Proxy is simply an object that our BigParticle will state should be inspected instead of the BigParticle itself. Objects state their intention to provide a proxy by implementing the Proxiable interface (which gives the single method propertiesProxy(). Change the BigParticle.java file as follows:

FROM...

public class BigParticle extends Particle
    {
CHANGE TO


public class BigParticle extends Particle implements Proxiable
    {
    // we can't "turn off" setRandomize by making it protected or whatnot.
    // but we can tell SimpleProperties to use a proxy of our invention
    // rather than querying us directly.  The proxy class MUST be public;
    // and if it's to be used in our model, it must be Serializable.
    // Also remember that if it's a non-static inner class, and we care
    // about cross-platform serialization, it needs to have a serialversionUID,
    // as well as its inclosing class!
    public class MyProxy
        {
        public int getXDir() { return xdir; }
        public int getYDir() { return ydir; }
        // because we are a non-static inner class
        static final long serialVersionUID = -2815745192429358605L;
        };
        
    // because we contain a non-static inner class
    static final long serialVersionUID = 7720089824883511682L;

    public Object propertiesProxy()
        {
        return new MyProxy();
        }

Save and compile the files, and run. Notice that now the Randomize checkbox has disappeared for BigParticle.

Provide a Model Inspector

Models can have a global inspector as well. We do this either overriding the GUIState.getInspector() method, or (easier) by overriding the GUIState.getSimulationInspectedObject() method, which provides an object to be inspected by a SimpleInspector.

To the Tutorial4WithUI.java file, add the method:


    public Object getSimulationInspectedObject()
        {
        return state;
        }

This tells the simulation system that state (our Tutorial4 object -- the model) should be checked for Java Bean Properties to display. At present we don't have any. In the Tutorial4.java file, change:

FROM...

public int gridWidth = 100;
public int gridHeight = 100;
public int numParticles = 500;
CHANGE TO

public int gridWidth = 100;
public int gridHeight = 100;
public int numParticles = 500;


public int getWidth() { return gridWidth; }
public void setWidth(int val) { if (val > 0 ) gridWidth = val; }
public int getHeight() { return gridHeight; }
public void setHeight(int val) { if (val > 0 ) gridHeight = val; }
public int getNumParticles() { return numParticles; }
public void setNumParticles(int val) { if (val >= 0) numParticles = val; }

Wow, with 20,000 Particles, and a width and height of 400 each, things really start to slow down!

Well, what did you expect? You've increased the number of Particles 40-fold and the number of trail squares 16-fold. The slowdown is due to several areas:

  1. 40 times as many agents to schedule.
  2. Accessing SparseGrid2D's hash tables 40 times as often -- start using an ObjectGrid2D instead?
  3. The arrays and hash tables may now be bigger than can be stored in cache. That's a big hit.
  4. The big one: drawing 40 times as many ovals and 16 times as many squares. That's expensive to say the least!

By the way, we've investigated moving from Java 2D arrays to Repast-style linearized (1D) arrays; for some problems it's faster with large arrays; for others (like HeatBugs) it's significantly slower. So it's still under investigation.

Save, compile, and run. Notice that the Console now has an extra tab called "Model". If you click on this tab, you can modify the width, height, and number of particles. Because these variables are only accessed by our model in its start() method, changing them won't have any effect until you stop and restart the model. Try it.

Make the Model Inspector Volatile

At present, the Model Inspector doesn't update itself as the Model runs. It only displays the initial values and lets you change them. What if you want to show up-to-the-minute statistics? You need a "volatile" inspector. Note that repainting a volatile inspector at every tick is significantly more expensive: so only do this if you need it.

First, let's add the need for such a thing. We'll keep track of "collisions" among the particles. To the Tutorial4.java file, add some extra stuff:


    public int collisions;
    public double collisionRate;
    public double getCollisionRate() { return collisionRate; }

Similarly, in the Particle.java file, change:

FROM...

if (randomize)
    {
    xdir = tut.random.nextInt(3) - 1;
    ydir = tut.random.nextInt(3) - 1;
    randomize = false;
    }
CHANGE TO

if (randomize)
    {
    xdir = tut.random.nextInt(3) - 1;
    ydir = tut.random.nextInt(3) - 1;
    randomize = false;
    tut.collisions++;
    }

Now we update the collision rate using the collisions. In the Tutorial4.java file, change:

FROM...

Steppable decreaser = new Steppable()
    {
    public void step(SimState state)
        {
        // decrease the trails
        trails.multiply(0.9);
        }
CHANGE TO

Steppable decreaser = new Steppable()
    {
    public void step(SimState state)
        {
        // decrease the trails
        trails.multiply(0.9);
        
        // compute and reset the collision info
        collisionRate = collisions / (double)numParticles;
        collisions = 0;
        }

Now we have a reason for a volatile inspector -- we'd like to see how the collisionRate changes. To make the inspector volatile, we override GUIState's isInspectorVolatile method. Add to Tutorial4WithUI.java the following:


    public boolean isInspectorVolatile()
        {
        return true;
        }

Save, compile, and run. Click on the Model tab and note that the Collisions property updates in real-time.

Make a Custom Inspector

All an Inspector is is a JPanel with a single extra method: updateInspector(). This method is called whenever the model thinks the Inspector should update itself to reflect new changes in whatever it's inspecting. Beyond this, it's up to you.

The SimpleInspector subclasses make this pretty by providing property inspection. But sometimes you want more. In this example, let's add a JButton called "Roll the Dice" to our BigParticle. When you press this button, the BigParticle's location and direction will be randomized.

As mentioned earlier, Inspectors are provided by Portrayals through the getInspector() method. We need to customize our BigParticle's portrayal. To do this, let's create a new file called BigParticleInspector.java. To this, we add:


package sim.app.tutorial4;
import sim.portrayal.*;
import sim.portrayal.grid.*;
import sim.field.grid.*;
import java.awt.event.*;
import java.awt.*;
import javax.swing.*;
import sim.engine.*;
import sim.util.*;
import sim.display.*;

public class BigParticleInspector extends Inspector
    {
    public Inspector originalInspector;
    
    public BigParticleInspector(Inspector originalInspector,
                                LocationWrapper wrapper,
                                GUIState guiState)
        {
        this.originalInspector = originalInspector;
        
        // get info out of the wrapper
        SparseGrid2DPortrayal gridportrayal = (SparseGrid2DPortrayal) wrapper.getFieldPortrayal();
        // these are final so that we can use them in the anonymous inner class below...
        final SparseGrid2D grid = gridportrayal.field;
        final BigParticle particle = (BigParticle) wrapper.getObject();
        final SimState state = guiState.state;
        final Controller console = guiState.controller;  // The Console (it's a Controller subclass)

Here we are making a new inspector which will build on the original inspector (a SimpleInspector), because we'd like to keep the original one around to display Xdir and Ydir, what the heck. As shown, we can use the wrapper to extract the inspected object and the field portrayal it's in. From the field portrayal we can extract the SparseGrid2D. Also, from the GUIState, we can extract the SimState object and the Console (a Controller subclass). Continuing:


        // now let's add a Button
        Box box = new Box(BoxLayout.X_AXIS);
        JButton button = new JButton("Roll the Dice");
        box.add(button);
        box.add(box.createGlue());

        // set up our inspector: keep the properties inspector around too
        setLayout(new BorderLayout());
        add(originalInspector, BorderLayout.CENTER);
        add(box, BorderLayout.NORTH);

...our inspector will hold the Roll the Dice button, plus the old inspector. Now we say what the button does:


        // set what the button does
        button.addActionListener(new ActionListener()
            {
            public void actionPerformed(ActionEvent e)
                {
                synchronized(state.schedule)
                	{
	                // randomize direction
	                particle.xdir = state.random.nextInt(3) - 1;
	                particle.ydir = state.random.nextInt(3) - 1;
	                
	                // randomize location
	                grid.setObjectLocation(particle,
	                    new Int2D(state.random.nextInt(grid.getWidth()),
	                              state.random.nextInt(grid.getHeight())));
	                
	                // update everything: console, inspectors, displays,
	                // everything that might be affected by randomization
	                console.refresh();
	                }
                }
            });
        }

The synchronized(state.schedule) directive is there for an important reason. The button is being pressed in the Swing Event Thread. The model that it will be modifying may be running in the underlying model thread. This is a sure-fire race condition.

I need to tweak a Swing Widget. What about calling the Swing Event Loop from inside the Model Thread?

You can only do so asynchronously. Create a Runnable, then pass it to SwingUtilities.invokeLater(). It'll get called in the Swing Event Loop. Note that this Runnable shouldn't then turn around and attempt to acquire objects in the model, unless it does so locking on state.schedule or using Controller.doChangeCode(...). In general the best approach is to load up the Runnable with copies of stuff it needs to tell Swing beforehand, so it doesn't need to ask for them.

Because there are two threads going on, we have to be careful. Since we're acting from the Swing Event Thread, we have three options: two synchronous, one asynchronous:

  1. (Synchronous) Lock on state.schedule. The underlying model locks on this lock before it does any work; so grabbing it will synchronize us.
  2. (Synchronous) Pass a Runnable to Controller.doChangeCode(...). This in effect stops the model thread at the next time tick, runs our code, then starts the model thread again. This is the "guaranteed" way to do it, but it's heavyweight -- usually locking on state.schedule is sufficient.
  3. (Asynchronous) Load a Steppable with copies of the relevant information to send to the model, then schedule it as GUIState.scheduleImmediate(). The Steppable will be called from inside the Model thread.

    Elsewhere in the simulator's documentation (the "notes.html" page) is more discussion about how to handle threading issues.

    No I forgot about that.

    Well, pay attention next time!

    The refresh() method updates all inspectors and all registered displays or other registered windows. Remember in Tutorial 2 when we said you should always register any window that could get updated by the model?

    refresh() is very expensive: only use it in response to user events; don't refresh in a tight loop. Also, never call this method from inside the model thread -- only from the Swing Event loop.

    Since we're an inspector, we need to implement updateInspector(). Easy:

    
        public void updateInspector()
            {
            originalInspector.updateInspector();
            }
        }
    

    Last, we need to create this inspector from a customized SimplePortrayal subclass for our BigParticle. All that needs to be done is overriding the getInspector method mentioned earlier in Tutorial 3. In the Tutorial4WithUI.java file, change:

    FROM...
    
    particlesPortrayal.setPortrayalForClass(
        BigParticle.class, new sim.portrayal.simple.SquarePortrayal2D(Color.red, 1.5) );
    
    CHANGE TO
    
    particlesPortrayal.setPortrayalForClass(
        BigParticle.class, new sim.portrayal.simple.SquarePortrayal2D(Color.red, 1.5)
            {
            public Inspector getInspector(LocationWrapper wrapper, GUIState state)
                {
                // make the inspector
                return new BigParticleInspector(super.getInspector(wrapper,state), wrapper, state);
                }
            });
    

    Hey, it moved in the "Other Display" too.

    Yes, but it's not red there, or bigger than the others. Can you figure out why?

    Save the files, compile and run. When you can, double-click on the BigParticle and note that its inspector now has a button. Press the button and the inspector's location and direction changes.

    Optionally Update the Serial Versions

    Wait, there are other anonymous classes now!

    True. BigParticleInspector has one, and Tutorial4WithUI has one. But neither of them are part of the model (they're not serialized). So no need to give them UIDs.

    Classnames have changed, and so the serialVersionUID values are technically "incorrect". If you really feel the need for it, you might re-run serialver and change the values. On MacOS X, they're 6976157378487763326L for the Decreaser, and 6930440709111220430L for Tutorial4.