Tutorial 5: Build a Simulation with Continuous and Network Fields

In this tutorial we will build a simple pseudo-spring-mass simulation using mass balls and simulated "rubber bands". The balls will start at different locations and have random mass values. When balls get close to one another they turn red; balls cannot collide (they'll just pass through one another). The rubber bands will connect random balls together, will have random strength values, and will have randomly-chosen "lax" lengths. The rubber bands will follow Hook's law, which says that the force of a rubber band is proportional to the length of the band minus the lax length, all times the strength.

This tutorial teaches:

Write the Rubber Band class

The rubber band class is the easiest to write. Make a directory called sim/app/tutorial5. In it, create a file called Band.java. In this file, write:


package sim.app.tutorial5;

public class Band implements java.io.Serializable
    {
    public double laxDistance;
    public double strength;
    
    public Band(double laxDistance, double strength)
        { this.laxDistance = laxDistance; this.strength = strength; }

    // Bean Properties for our Inspector
    public void setStrength(double val) { if (val > 0) strength = val; }
    public double getStrength() { return strength; }
    public void setLaxDistance(double val) { if (val >= 0) laxDistance = val; }
    public double getLaxDistance() { return laxDistance; }
    }

Straightforward enough.

Write the Model

We'll write the model class (Tutorial5) before writing the Ball class -- it's easier to explain things that way. Create a file called Tutorial5.java. In it add:


package sim.app.tutorial5;
import sim.engine.*;
import sim.field.continuous.*;
import sim.field.network.*;
import sim.util.*;
import ec.util.*;

public class Tutorial5 extends SimState
    {
    public Continuous2D balls;
    public NetworkField bands;

    public int numBalls = 50;
    public int numBands = 60;
    
    public final static double maxMass = 10.0;
    public final static double minMass = 1.0;
    public final static double minLaxBandDistance = 10.0;
    public final static double maxLaxBandDistance = 50.0;
    public final static double minBandStrength = 5.0;
    public final static double maxBandStrength = 10.0;
    public final static double collisionDistance = 5.0;
    
    public int getNumBalls() { return numBalls; }
    public void setNumBalls(int val) { if (val >= 2 ) numBalls = val; }
    public int getNumBands() { return numBands; }
    public void setNumBands(int val) { if (val >= 0 ) numBands = val; }

    public Tutorial5(long seed)
        {
        super(new MersenneTwisterFast(seed), new Schedule(2));
        balls = new Continuous2D(collisionDistance,100,100);
        bands = new NetworkField();
        }

So objects can belong in more than one field?

Yes. There is only one exception: Edges relating objects in a NetworkField can only be associated with a given NetworkField. To relate the same two objects in two different NetworkFields, you must use two different Edges. Of course any Edge's info object can be reused in different Fields.

We're storing the Balls in a Continuous2D class. This is a SparseField which relates arbitrary objects to real-valued (Double2D) locations. Additionally, we'll be adding those same Balls into a NetworkField, and relating them with Edges which store, in their info fields, the Bands that connect them. An Edge is the class that defines relationships within a NetworkField. You can make up your own Edge subclasses if you like, but we'll use the default ones which are automagically created with the addEdge(...) method.

Why pass collisionDistance to the Continuous2D constructor?

Continuous2D stores objects not only with a Double2D location, but also puts them in a discretized grid associated with an Int2D. This is to allow you to efficiently get all objects located within some distance of an object in a Continuous2D. The choice of discretization depends on several factors, but if your objects are just point objects (like our Balls will be) then usually you want a discretization about the same size as the typical lookup-within-distance you'll be asking the Continuous2D for. In our case, that's collisionDistance.

For more information, look up the documentation on Continuous2D.

Now we'll create the balls. Each ball will be its own agent. When its step() method is called, a Ball will move itself according to its current force. Additionally, we want to schedule another agent to compute and update the Ball's current forces. The "force-computers" should be scheduled all after the Balls; thus they're placed in Order 1 of the schedule. Additionally, the force-computers don't have to have random order in movement (technically the Balls don't either). This allows us to demonstrate a new feature of the schedule: Sequences.

A Sequence is a Steppable which contains an array of Steppables. When it's stepped, it steps all its Steppables in turn. Plain Sequences step their Steppables in the order of the array, but a RandomSequence will randomize the array first, and a ParallelSequence will step each one in a separate thread (be careful with that one -- only use it if you know what you're doing threadwise). There are other kinds of special Steppables available: a WeakStep contains a single Steppable and steps it but only holds onto it as a java.lang.ref.WeakReference (it can be garbage-collected). A MultiStep contains a single steppable, and either steps it N times when the MultiStep is stepped, or steps it once every N times that the MultiStep is stepped.

So there's no way to have two different "step()" methods for an Agent registered in a Schedule?

Nope. An agent can only "do one thing". But it's not an issue. Just make a different agent which does the job -- often just a little anonymous Steppable which calls the right function like we're doing here.

Because "force-computers" do a different thing than Balls do when stepped, we can't re-use the Balls' step methods. So instead we'll make some anonymous Steppables to call computeForce() on the Balls.


    public void start()
        {
        super.start();

        balls = new Continuous2D(collisionDistance,100,100);
        bands = new NetworkField();
        
        Steppable[] s = new Steppable[numBalls];
        
        // make the balls
        for(int i=0; i<numBalls;i++)
            {
            // must be final to be used in the anonymous class below
            final Ball ball = new Ball(0,0,random.nextDouble() * (maxMass-minMass) + minMass);
            balls.setObjectLocation(ball,
                                    new Double2D(random.nextDouble() * 100,
                                                 random.nextDouble() * 100));
            bands.addNode(ball);
            schedule.scheduleRepeating(ball);
            
            // schedule the balls to compute their force after everyone's moved
            s[i] = new Steppable()
                {
                public void step(SimState state) { ball.computeForce(state); }
                // see Tutorial 3 for why this is helpful
                static final long serialVersionUID = -4269174171145445918L;
                };
            }
            
        // add the sequence
        schedule.scheduleRepeating(Schedule.EPOCH,1,new Sequence(s),1);

Now we make the Bands. All the Balls have been dumped into the NetworkField already -- all we need to do is build Edges connecting two balls, labelling the Edges with a given Band. We'll add random rubber bands. To keep it simple, it's possible that two edges could connect the same two Balls.


        // make the bands
        Bag ballObjs = balls.getAllObjects();
        for(int i=0;i<numBands;i++)
            {
            Band band = new Band(random.nextDouble() * 
                                (maxLaxBandDistance - minLaxBandDistance) + minLaxBandDistance,
                            random.nextDouble() *
                                (maxBandStrength - minBandStrength) + minBandStrength);
            Ball from;
            from = (Ball)(ballObjs.objs[random.nextInt(ballObjs.numObjs)]);

            Ball to = from;
            while(to == from)
                to = (Ball)(ballObjs.objs[random.nextInt(ballObjs.numObjs)]);
            
            bands.addEdge(from,to,band);
            }
        }

Finish up with a boilerplate main() which we won't use anyway -- this Tutorial is for visualization. Plus some serial UID stuff.


    public static void main(String[] args)
        {
        Tutorial5 tutorial5 = 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 Tutorial5))
                    {
                    System.out.println("Checkpoint contains some other simulation: " + state);
                    System.exit(1);
                    }
                else tutorial5 = (Tutorial5)state;
                }
        if (tutorial5==null)
            {
            tutorial5 = new Tutorial5(System.currentTimeMillis());
            tutorial5.start();
            }
        long time;
        while((time = tutorial5.schedule.time()) < 10000)
            {
            if (time % 10 == 0) System.out.println(time);
            if (!tutorial5.schedule.step(tutorial5)) break;
            if (time%500==0 && time!=0)
                {
                String s = "tutorial5." + time + ".checkpoint";
                System.out.println("Checkpointing to file: " + s);
                tutorial5.writeToCheckpoint(new java.io.File(s));
                }
            }
        tutorial5.finish();
        }
        
        // see Tutorial 3 for why this is helpful
        static final long serialVersionUID = -7164072518609011190L;
    }

Write the Ball class

This is where most of the physics lies. Create a file called Ball.java. In it, add:


package sim.app.tutorial5;
import sim.engine.*;
import sim.portrayal.*;
import sim.util.*;
import sim.field.network.*;
import sim.field.continuous.*;
import java.awt.*;
import java.awt.geom.*;

public class Ball implements Steppable
    {
    // force on the Ball
    public double forcex;
    public double forcey;
    
    // Ball mass
    public double mass;
    
    // Current Ball velocity
    public double velocityx;
    public double velocityy;
    
    // did the Ball collide?
    public boolean collision;
    
    // for drawing: always sqrt of mass
    public double diameter;
        
    public double getVelocityX() { return velocityx; }
    public void setVelocityX(double val) { velocityx = val; }
    public double getVelocityY() { return velocityy; }
    public void setVelocityY(double val) { velocityy = val; }
    public double getMass() { return mass; }
    public void setMass(double val) { if (val > 0) { mass = val; diameter = Math.sqrt(val); } }
    
    public Ball(double vx, double vy, double m)
        {
        velocityx=vx;
        velocityy=vy;
        mass = m;
        diameter = Math.sqrt(m);
        }

Our balls have a mass, a velocity in x and y, a current force in x and y, a diameter to display (determined by the mass), and a flag indicating whether or not they've collided on this pass. When a Ball is stepped, it will apply its current forces, adjusting its velocity and moving one step accordingly. It will also determine if it's collided with other objects.

We'll start with the collision computation. What we'll do is grab all the balls within the "collision distance" from the Continuous2D field using getObjectsWithinDistance(...). This method is liberal and may return objects that are well outside the desired distance (it's just using the discretized buckets to return objects). We then examine the objects one by one to see if they're within the distance, and if so, set collision flags in them and in us. You get the location of a given object in the field, as expected, with getObjectLocation(...).


    public void computeCollision(Tutorial5 tut)
        {
        collision = false;
        Double2D me = tut.balls.getObjectLocation(this);
        Bag b = tut.balls.getObjectsWithinDistance(me,Tutorial5.collisionDistance);
        for(int x=0;x<b.numObjs;x++)
            if( this != b.objs[x] )
                {
                Double2D loc = tut.balls.getObjectLocation(b.objs[x]);
                if ((loc.x-me.x)*(loc.x-me.x) + (loc.y-me.y)*(loc.y-me.y) 
                    <= Tutorial5.collisionDistance * Tutorial5.collisionDistance)
                        {
                        collision = true;
                        ((Ball)(b.objs[x])).collision = true;
                        }
                }
        }

Now let's take the step. Once we compute the velocity, we get the current position using getObjectLocation, then set the new position with setObjectLocation. Notice the similarity to SparseGrid2D. This is because both SparseGrid2D and Continuous2D are subclasses of SparseField.


    public void step(SimState state)
        {
        Tutorial5 tut = (Tutorial5) state;
        
        // acceleration = force / mass
        final double ax = forcex / mass;
        final double ay = forcey / mass;
        
        // velocity = velocity + acceleration
        velocityx += ax;
        velocityy += ay;
        
        // position = position + velocity
        Double2D pos = tut.balls.getObjectLocation(this);
        Double2D newpos = new Double2D(pos.x+velocityx, pos.y + velocityy);
        tut.balls.setObjectLocation(this,newpos);
        
        // compute collisions
        computeCollision(tut);
        }

Next we need to implement the computeForce(...) funtion used by the force-computer in the Tutorial5 class. We begin with a function that adds the force from a given rubber band connecting us with another ball.


    public void addForce(Double2D otherBallLoc, Double2D myLoc, Band band)
        {
        // compute difference
        final double dx = otherBallLoc.x - myLoc.x;
        final double dy = otherBallLoc.y - myLoc.y;
        final double len = Math.sqrt(dx*dx + dy*dy);
        final double l = band.laxDistance;

        //Hook's law
        final double k = band.strength/512.0;  // cut down reasonably
        final double forcemagnitude = (len - l) * k;
        
        // add rubber band force
        if (len - l > 0) 
            {
            forcex += (dx * forcemagnitude) / len;
            forcey += (dy * forcemagnitude) / len;
            }
        }

Now to implement the computeForce(...) function, we go to the NetworkField, extract all the edges going into or coming out of us, getting the rubber bands and the "other Ball"s out of those edges, then calling addForce with each of them. You extract edges from the NetworkField with getEdgesIn(obj) and getEdgesOut(obj). The other ball will be in the edge's from() field or to() field respectively, and the Band will be in the object's info field. Note that from and to are called with functions -- they can't be changed -- but info can be accessed simply by accessing (or modifying at any time) the slot directly.


    public void computeForce(SimState state)
        {
        Tutorial5 tut = (Tutorial5) state;
        NetworkField bands = tut.bands;
        Continuous2D balls = tut.balls;

        Double2D me = balls.getObjectLocation(this);
        
        forcex = 0; forcey = 0;
        // rubber bands exert a force both ways --
        // so our graph is undirected.  We need to get edges
        // both in and out, as they could be located either place
        Bag in = bands.getEdgesIn(this);
        Bag out = bands.getEdgesOut(this);
        if (in!=null)
            for(int x=0;x// from him to me
                Double2D him = balls.getObjectLocation(other);
                addForce(him,me,b);
                }
        if (out!=null)
            for(int x=0;x// from me to him
                Double2D him = balls.getObjectLocation(other);
                addForce(him,me,b);
                }
        }
    }

Add the Visualization

Create a file called Tutorial5WithUI.java. We'll start with some boilerplate you've seen before:

package sim.app.tutorial5;
import sim.portrayal.network.*;
import sim.portrayal.continuous.*;
import sim.engine.*;
import sim.display.*;
import javax.swing.*;
import java.awt.Color;

public class Tutorial5WithUI extends GUIState
    {
    public Display2D display;
    public JFrame displayFrame;

    Network2DPortrayal edgePortrayal = new Network2DPortrayal();
    Continuous2DPortrayal nodePortrayal = new Continuous2DPortrayal();

    public static void main(String[] args)
        {
        Tutorial5WithUI vid = new Tutorial5WithUI();
        Console c = new Console(vid);
        c.setVisible(true);
        }

    public Tutorial5WithUI() { super(new Tutorial5( System.currentTimeMillis())); }
    public Tutorial5WithUI(SimState state) { super(state); }

    public String getName() { return "Tutorial 5"; }
    
    public String getInfo() { return "<H2>Tutorial 5</H2> Balls and Rubber Bands!"; }

    public Object getSimulationInspectedObject() { return state; }

    public void start()
    {
        super.start();
        setupPortrayals();
    }

    public void load(SimState state)
        {
        super.load(state);
        setupPortrayals();
        }

Now we need to set up the portrayals. The Continuous2D is portrayed straightforwardly enough. But the NetworkField needs some explanation. NetworkField2DPortrayal only draws edges! Nodes must be stored in another field, either a SparseGrid2D or a Continuous2D. This field must be passed to a container object called SpatialNetwork2D, which is in turn handed to the NetworkField2DPortrayal. The locations of the nodes in the SparseGrid2D or Continuous2D determine where the edges are drawn. But you'll need to draw the nodes yourself by using a SparseGrid2DPortrayal or Continuous2DPortrayal respectively. I suggest you draw the edges first, then the nodes on top of them. Here we go:


    public void setupPortrayals()
        {
        Tutorial5 tut = (Tutorial5) state;
        
        // tell the portrayals what to portray and how to portray them
        edgePortrayal.setField( new SpatialNetwork2D( tut.balls, tut.bands ) );
        edgePortrayal.setPortrayalForAll(new BandPortrayal2D());
        nodePortrayal.setField( tut.balls );

        // reschedule the displayer
        display.reset();
        display.setBackdrop(Color.white);

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

Notice that we specified a special SimplePortrayal to draw our Edges. This is a subclass of EdgePortrayal2D which we will write, called BandPortrayal2D.java. More on that later.

Notice also that no portrayal was specified for the Balls. This is because we'll let the Balls portray themselves. More on that later as well. We'll finish up with typical methods:


    public void init(Controller c)
        {
        super.init(c);

        // make the displayer
        display = new Display2D(600,600,this,1);

        displayFrame = display.createFrame();
        displayFrame.setTitle("Tutorial 5 Display");
        c.registerFrame(displayFrame);   // register the frame so it appears in the "Display" list
        displayFrame.setVisible(true);
        display.attach( edgePortrayal, "Bands" );
        display.attach( nodePortrayal, "Balls" );
        }

    public void quit()
        {
        super.quit();

        if (displayFrame!=null) displayFrame.dispose();
        displayFrame = null;
        display = null;
        }
    }

Let the Balls Portray Themselves

Objects can portray themselves if they're subclasses of SimplePortrayal2D. Open the Ball.java file, and change:

FROM...

public class Ball implements Steppable
CHANGE TO

public class Ball extends SimplePortrayal2D implements Steppable

Couldn't we just use sim.portrayal.simple.OvalPortrayal2D?

Yes, but we'll roll our own for tutorial purposes.

SimplePortrayal provides default versions for most Portrayal2D methods. But we'll override two of them: drawing the Ball and doing hit-testing on the Ball for mouse-clicking.

Why not use Graphics2D Affine Transforms?

They are very very very slow. Also, font sizes are transformed, so if you draw text labels, then scale it up, the text gets bigger -- usually that's not what you want. Similarly, lines get thicker, etc. Instead we give you the scale information and you an do what you like with it.

The draw(...) method takes an Object to draw, a Graphics2D, and a DrawInfo2D object. DrawInfo2D contains two rects: draw tells us the origin (x,y) and the scale (in width and height) of our coordinate system. Most simple objects are drawn within a width-by-height box centered on the origin. Fields are drawn within a width-by-height box but whose top-right corner is the origin. The other rect is clip, which tells us the region that needs to be drawn. Simple objects likely won't get their draw() method called unless they need to be drawn, so we can ignore the clip. Fields use the clip extensively to figure out which simple objects within them need to be drawn.

We'll draw Balls using the diameter parameter we defined early on. Add to Ball.java the function:


    public void draw(Object object, Graphics2D graphics, DrawInfo2D info)
        {
        final double width = info.draw.width * diameter;
        final double height = info.draw.height * diameter;

        if (collision) graphics.setColor(Color.red);
        else graphics.setColor(Color.blue);

        final int x = (int)(info.draw.x - width / 2.0);
        final int y = (int)(info.draw.y - height / 2.0);
        final int w = (int)(width);
        final int h = (int)(height);

        // draw centered on the origin
        graphics.fillOval(x,y,w,h);
        }

Why do we draw with fillOval instead of using an Ellipse2D?

Ellipse2D, like most of Java2D, is much slower than fillOval (or other java.awt.Graphics primitives).

Similarly, we need to override the hitObject() method, which takes an Object and a DrawInfo2D. This method tells us if an object drawn at the provided origin and scale intersects with the provided clip region; perhaps a mouse click, or a dragged rect selection by the mouse. The easiest way to do this is with Java2D objects, in this case Ellipse2D. Add to Ball.java:


    public boolean hitObject(Object object, DrawInfo2D range)
        {
        final double SLOP = 1.0;  // need a little extra diameter to hit circles
        final double width = range.draw.width * diameter;
        final double height = range.draw.height * diameter;
        
        Ellipse2D.Double ellipse = new Ellipse2D.Double( 
                range.draw.x-width/2-SLOP, 
                range.draw.y-height/2-SLOP, 
                width+SLOP*2,
                height+SLOP*2 );
        return ( ellipse.intersects( range.clip.x, range.clip.y, range.clip.width, range.clip.height ) );
        }

Make a Custom Edge Portrayal

Now we need to write an SimpleEdgePortrayal2D subclass to draw our Rubber Bands. The default SimpleEdgePortrayal2D class draws edges directed (red on one end, black on the other), with info.toString() as a label in gray in the middle. That's not quite what we want. We'd like it to be black everywhere, with a small strength value in the center, drawn in blue. So we'll make our own version.

Create a file called BandPortrayal2D.java. In this file, add:


package sim.app.tutorial5;
import sim.field.network.*;
import sim.portrayal.network.*;
import sim.portrayal.*;
import java.awt.*;

public class BandPortrayal2D extends SimpleEdgePortrayal2D
    {
    // how our strength should look
    java.text.DecimalFormat strengthFormat = new java.text.DecimalFormat("#0.##");

    public void draw(Object object, Graphics2D graphics, DrawInfo2D info)
        {

So far, looks like an ordinary draw method. But Edges expect that their DrawInfo2D be actually an EdgeDrawInfo2D object, which adds a second drawing point (the first one is at the (x,y) origin) to define the line. Add:


        // this better be an EdgeDrawInfo2D!  :-)
        EdgeDrawInfo2D ei = (EdgeDrawInfo2D) info;
        // likewise, this better be an Edge!
        Edge e = (Edge) object;

        // our start (x,y), ending (x,y), and midpoint (for drawing the label)
        final int startX = (int)ei.draw.x;
        final int startY = (int)ei.draw.y;
        final int endX = (int)ei.secondPoint.x;
        final int endY = (int)ei.secondPoint.y;
        final int midX = (int)((ei.draw.x+ei.secondPoint.x) / 2);
        final int midY = (int)((ei.draw.y+ei.secondPoint.y) / 2);

Now we can draw the line.


        // draw line
        Band b = (Band)(e.info);
        graphics.setColor(Color.black);
        graphics.drawLine (startX, startY, endX, endY);
        
        // draw label in blue
        graphics.setColor(Color.blue);
        graphics.setFont(labelFont);  // default font for Edge labels
        String information = strengthFormat.format(((Band)(e.info)).strength);
        int width = graphics.getFontMetrics().stringWidth(information);
        graphics.drawString( information, midX - width / 2, midY );
        }
    
    // use the default hitObject -- don't bother writing that one, it works fine
    }

Run that Sucker

Save and compile all files, then run java sim.app.tutorial5.Tutorial5WithUI. Notice that the balls bounce well outside of the clip region (try scaling out). We'd like to be able to scale out and be able to see more of the region, even though it's "out of bounds" so to speak.

We can do this by telling Display2D to turn off its clip. In the Tutorial5WithUI.java file, change:

FROM...

display = new Display2D(600,600,this,1);
CHANGE TO

display = new Display2D(600,600,this,1);
// turn off clipping
display.setClipping(false);

Recompile the Tutorial5WithUI.java file, and re-run the application. Scale out and enjoy.